// ==UserScript== // @name NGA Cache History // @namespace https://greasyfork.org/users/263018 // @version 1.0.0 // @author snyssss // @description 将帖子内容缓存 IndexedDB 里,以便在帖子被审核/删除时仍能查看 // @license MIT // @match *://bbs.nga.cn/* // @match *://ngabbs.com/* // @match *://nga.178.com/* // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @noframes // @downloadURL none // ==/UserScript== (async ({ commonui: ui, _LOADERREAD: loader }) => { // 检查是否支持 IndexedDB if (window.indexedDB === undefined) { return; } // 常量 const KEY = "NGA_CACHE"; const VERSION = 1; const TABLE_NAME = "reads"; const EXPIRE_DURATION_KEY = "EXPIRE_DURATION"; // 缓存时长 const EXPIRE_DURATION = GM_getValue(EXPIRE_DURATION_KEY, 7); // 判断帖子是否正常 const isSuccess = () => { return ui; }; // 获取数据库实例 const db = await new Promise((resolve) => { // 打开 IndexedDB 数据库 const request = window.indexedDB.open(KEY, VERSION); // 如果数据库不存在则创建 request.onupgradeneeded = (event) => { // 创建表 const store = event.target.result.createObjectStore(TABLE_NAME, { keyPath: "url", }); // 创建索引,用于清除过期数据 store.createIndex("timestamp", "timestamp"); }; // 成功后返回实例 request.onsuccess = (event) => { resolve(event.target.result); }; }); // 删除数据 const expire = (store, offset) => { // 获取索引 const index = store.index("timestamp"); // 查找超时数据 const request = index.openCursor( IDBKeyRange.upperBound(Date.now() - offset) ); // 成功后删除数据 request.onsuccess = (event) => { const cursor = event.target.result; if (cursor) { store.delete(cursor.primaryKey); cursor.continue(); } }; }; // 缓存数据 const save = (url) => { // 只缓存帖子内容 if (location.pathname !== "/read.php") { return; } // 重新请求原始数据用于缓存 fetch(url) .then((res) => res.blob()) .then((res) => { // 读取内容 const reader = new FileReader(); reader.onload = () => { // 读取内容 const content = reader.result; // 解析标题 const parser = new DOMParser(); const html = parser.parseFromString(content, "text/html"); const title = html.querySelector("title").textContent; // 创建事务 const transaction = db.transaction([TABLE_NAME], "readwrite"); // 获取对象仓库 const store = transaction.objectStore(TABLE_NAME); // 写入数据 store.put({ url, title, content, timestamp: Date.now(), }); // 删除超时数据 expire(store, EXPIRE_DURATION * 24 * 60 * 60 * 1000); }; reader.readAsText(res, "GBK"); }); }; // 读取数据 const load = (url, document) => { // 只缓存帖子内容 if (location.pathname !== "/read.php") { return; } // 创建事务 const transaction = db.transaction([TABLE_NAME], "readonly"); // 获取对象仓库 const store = transaction.objectStore(TABLE_NAME); // 获取数据 const request = store.get(url); // 成功后处理数据 request.onsuccess = (event) => { // 获取页面对象 const data = event.target.result; // 不存在则跳过 if (data === undefined) { return; } // 加载缓存内容 const html = document.open("text/html", "replace"); html.write(data.content); html.close(); // 修改内容边框颜色以区分缓存 const anchor = document.querySelector("#m_posts"); if (anchor) { anchor.style.borderColor = "#591804"; } }; }; // STYLE GM_addStyle(` .s-table-wrapper { height: calc((2em + 10px) * 11 + 3px); overflow-y: auto; } .s-table { margin: 0; } .s-table th, .s-table td { position: relative; white-space: nowrap; } .s-table th { position: sticky; top: 2px; z-index: 1; } `); // UI const loadUI = () => { if (!ui) { return; } const content = (() => { const c = document.createElement("div"); c.innerHTML = `
时间 内容 操作
`; return c; })(); let position = null; let hasNext = true; let isFetching = false; const list = content.querySelector("TBODY"); const wrapper = content.querySelector(".s-table-wrapper"); const fetchData = () => { isFetching = true; // 声明查询数量 let limit = 10; // 创建事务 const transaction = db.transaction([TABLE_NAME], "readonly"); // 获取对象仓库 const store = transaction.objectStore(TABLE_NAME); // 获取索引 const index = store.index("timestamp"); // 查找数据 const request = index.openCursor( position ? IDBKeyRange.upperBound(position) : null, "prev" ); // 加载列表 request.onsuccess = (event) => { const cursor = event.target.result; if (cursor) { const { url, title, timestamp } = cursor.value; position = timestamp; if (list.querySelector(`[data-url="${url}"]`)) { cursor.continue(); return; } const item = document.createElement("TR"); item.className = `row${(list.querySelectorAll("TR").length % 2) + 1}`; item.setAttribute("data-url", url); item.innerHTML = ` ${ui.time2dis(timestamp / 1000)} ${title} `; const button = item.querySelector("button"); button.onclick = () => { const iWindow = ui.createCommmonWindow(); const iframe = document.createElement("IFRAME"); iframe.style.width = "80vw"; iframe.style.height = "80vh"; iframe.style.border = "none"; const iframeLoad = () => { iframe.removeEventListener("load", iframeLoad); load(url, iframe.contentDocument); }; iframe.addEventListener("load", iframeLoad); iWindow._.addTitle(title); iWindow._.addContent(iframe); iWindow._.show(); }; list.appendChild(item); if (limit > 1) { cursor.continue(); } else { isFetching = false; } } else { hasNext = false; } limit -= 1; }; }; const refetch = () => { list.innerHTML = ``; position = null; hasNext = true; isFetching = false; fetchData(); }; wrapper.onscroll = () => { if (isFetching || !hasNext) { return; } if (wrapper.scrollHeight - wrapper.scrollTop <= wrapper.clientHeight) { fetchData(); } }; // 增加菜单项 (() => { const title = "浏览记录"; let window; ui.mainMenu.addItemOnTheFly(title, null, () => { if (window === undefined) { window = ui.createCommmonWindow(); } refetch(); window._.addTitle(title); window._.addContent(content); window._.show(); }); })(); }; // 绑定事件 const hook = () => { // 钩子 const hookFunction = (object, functionName, callback) => { ((originalFunction) => { object[functionName] = function () { const returnValue = originalFunction.apply(this, arguments); callback.apply(this, [returnValue, originalFunction, arguments]); return returnValue; }; })(object[functionName]); }; // 页面跳转 if (loader) { hookFunction(loader, "go", (returnValue, originalFunction, arguments) => { if (arguments[1]) { const { url } = arguments[1]; save(url); } }); } // 快速翻页 if (ui) { hookFunction( ui, "loadReadHidden", (returnValue, originalFunction, arguments) => { if (arguments && __PAGE) { const p = (() => { if (arguments[1] & 2) { return __PAGE[2] + 1; } if (arguments[1] & 4) { return __PAGE[2] - 1; } return arguments[0]; })(); if (p < 1 || (__PAGE[1] > 0 && p > __PAGE[1])) { return; } const urlParams = new URLSearchParams(window.location.search); urlParams.set("page", p); const url = `${window.location.origin}${ window.location.pathname }?${urlParams.toString()}`; save(url); } } ); } }; // 加载菜单项 (() => { GM_registerMenuCommand(`缓存天数:${EXPIRE_DURATION} 天`, () => { const input = prompt("请输入缓存天数(最大1000):", EXPIRE_DURATION); if (input) { const duration = parseInt(input, 10); if (duration > 0 && duration <= 1000) { GM_setValue(EXPIRE_DURATION, duration); location.reload(); } } }); })(); // 执行脚本 (() => { // 绑定事件 hook(); // 加载UI loadUI(); // 当前链接地址 const url = window.location.href; // 帖子正常的情况下缓存数据,否则尝试从缓存中读取 if (isSuccess()) { save(url); } else { load(url, document); } })(); })(unsafeWindow);