您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
支持搜同网和TT1069论坛的帖子访问记录标记功能,点击即标记为已读,并支持导出导入清除阅读记录,记录发帖时间,防止丢失。
当前为
// ==UserScript== // @name Multi Forum Read Marker (Soutong + TT1069) // @namespace https://felixchristian.dev/userscripts/multi-forum-read-marker // @version 1.6.0 // @description 支持搜同网和TT1069论坛的帖子访问记录标记功能,点击即标记为已读,并支持导出导入清除阅读记录,记录发帖时间,防止丢失。 // @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'; let visitedTids = {}; function getTidFromUrl(url) { try { const u = new URL(url, location.origin); let tid = u.searchParams.get('tid'); if (!tid) { const match = url.match(/thread-(\d+)-/); tid = match ? match[1] : null; } return tid; } catch (e) { return null; } } function loadVisited() { try { visitedTids = GM_getValue(STORAGE_KEY, {}); const localData = localStorage.getItem(STORAGE_KEY + '_temp'); if (localData) { const localObj = JSON.parse(localData); visitedTids = {...visitedTids, ...localObj}; } } catch (e) { visitedTids = {}; } } function saveVisited() { try { GM_setValue(STORAGE_KEY, visitedTids); localStorage.setItem(STORAGE_KEY + '_temp', JSON.stringify(visitedTids)); } catch (e) { console.error('保存失败:', e); } } function exportVisitedData() { const dataStr = JSON.stringify(visitedTids, null, 2); const blob = new Blob([dataStr], {type: "application/json"}); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `visitedTids_backup_${Date.now()}.json`; a.click(); URL.revokeObjectURL(url); } function importVisitedData(jsonText) { try { const newData = JSON.parse(jsonText); if (typeof newData === 'object') { visitedTids = {...visitedTids, ...newData}; saveVisited(); alert("导入成功!"); location.reload(); } else { alert("导入失败:格式错误"); } } catch (e) { alert("导入失败:JSON解析错误"); } } function clearVisitedData() { if (confirm("确定要清除所有已读记录吗?")) { visitedTids = {}; saveVisited(); alert("已清除!"); location.reload(); } } function addImportExportUI() { const container = document.createElement("div"); container.style.position = "fixed"; container.style.bottom = "10px"; container.style.right = "10px"; container.style.zIndex = "9999"; container.style.backgroundColor = "#fff"; container.style.border = "1px solid #888"; container.style.padding = "5px"; container.style.fontSize = "12px"; const exportBtn = document.createElement("button"); exportBtn.textContent = "导出记录"; exportBtn.onclick = exportVisitedData; const importBtn = document.createElement("button"); importBtn.textContent = "导入记录"; importBtn.style.marginLeft = "5px"; importBtn.onclick = () => { const input = document.createElement("input"); input.type = "file"; input.accept = ".json"; input.onchange = e => { const file = e.target.files[0]; if (file) { const reader = new FileReader(); reader.onload = () => { importVisitedData(reader.result); }; reader.readAsText(file); } }; input.click(); }; const clearBtn = document.createElement("button"); clearBtn.textContent = "清除记录"; clearBtn.style.marginLeft = "5px"; clearBtn.onclick = clearVisitedData; container.appendChild(exportBtn); container.appendChild(importBtn); container.appendChild(clearBtn); document.body.appendChild(container); } loadVisited(); function markReadThreads() { const threadLinks = document.querySelectorAll('a.s.xst'); threadLinks.forEach(link => { if (link.dataset.markedVisited) return; const tid = getTidFromUrl(link.href); if (tid && visitedTids[tid]) { 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'; } }); } function attachClickListeners() { const threadLinks = document.querySelectorAll('a.s.xst'); threadLinks.forEach(link => { if (link.dataset.clickListenerAdded) return; link.addEventListener('click', () => { const tid = getTidFromUrl(link.href); if (tid && !visitedTids[tid]) { visitedTids[tid] = { visitedAt: Date.now() }; saveVisited(); } }); link.dataset.clickListenerAdded = 'true'; }); } // 添加发帖时间(在帖子页面中) function extractPostTime() { const tid = getTidFromUrl(location.href); if (!tid) return; const em = document.querySelector('div.authi em[id^="authorposton"]'); if (em && em.textContent.includes("发表于")) { const dateText = em.textContent.replace("发表于", "").trim(); if (!visitedTids[tid]) visitedTids[tid] = {}; visitedTids[tid].postDate = dateText; visitedTids[tid].visitedAt = visitedTids[tid].visitedAt || Date.now(); saveVisited(); } } // 在论坛页 if (location.href.includes('forumdisplay') || /forum-\d+-\d+\.html/.test(location.pathname)) { window.addEventListener('load', () => { markReadThreads(); attachClickListeners(); addImportExportUI(); }); const observer = new MutationObserver(() => { markReadThreads(); attachClickListeners(); }); observer.observe(document.body, {childList: true, subtree: true}); } // 在帖子详情页 if (location.href.includes('mod=viewthread') || /thread-\d+-/.test(location.pathname)) { const tid = getTidFromUrl(location.href); if (tid) { if (!visitedTids[tid]) visitedTids[tid] = {}; visitedTids[tid].visitedAt = Date.now(); saveVisited(); extractPostTime(); } } })();