支持搜同网和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();
}
}
})();