您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
支持搜同网和TT1069论坛的帖子访问记录标记功能,点击即标记为已读,记录可读格式访问时间与发帖时间,支持导入导出合并、清除记录并持久保存。
当前为
// ==UserScript== // @name Multi Forum Read Marker // @namespace https://felixchristian.dev/userscripts/multi-forum-read-marker // @version 1.6.1 // @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 formatTime(ts = Date.now()) { const d = new Date(ts); const pad = (n) => n.toString().padStart(2, '0'); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; } 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 sortedEntries = Object.entries(visitedTids).sort((a, b) => { return new Date(b[1].visitedAt) - new Date(a[1].visitedAt); }); const dataStr = JSON.stringify(Object.fromEntries(sortedEntries), 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) { if (!visitedTids[tid]) visitedTids[tid] = {}; visitedTids[tid].visitedAt = formatTime(); 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 || formatTime(); 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 = formatTime(); saveVisited(); extractPostTime(); } } })();