您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Save hashes of displayed comments locally and mark new ones when displayed for the first time
当前为
// ==UserScript== // @name HN: style unread content // @description Save hashes of displayed comments locally and mark new ones when displayed for the first time // @namespace myfonj // @match https://news.ycombinator.com/* // @grant none // @version 1.0.4 // @author myfonj // ==/UserScript== // https://gf.qytechs.cn/en/scripts/423969/versions/new // kinda sorta configuration const SELECTOR = '.commtext, .storylink'; const VIEWPORT_EXPOSITION_DURATION_UNTIL_READ = 900; const CSS_CLASSES = { unread: 'new', read: 'read', old: 'old' }; // Styling. Very lame for now. const CSS_STR = ` .${CSS_CLASSES.unread} { border-right: 2px solid #3F36; display: block; padding-right: 1em; } .${CSS_CLASSES.read} { border-right: 2px solid #0F03; display: block; padding-right: 1em; } .${CSS_CLASSES.old} { /* nothing */ } `; // base64 'SHA-1' digest hash = 28 characters; most probably (always?) ending with '=' // could be used for splitting perhaps, but not used for it now. const HASH_DIGEST_ALGO = 'SHA-1'; // actual code, yo document.head.appendChild(document.createElement('style')).textContent = CSS_STR; // TODO mutation observer for client-side rendered pages // not case for HN, but it will make this truly universal const ELS_TO_WATCH = document.querySelectorAll(SELECTOR); const MAP_EL_HASH = new WeakMap(); const MAP_EL_TIMEOUT = new WeakMap(); const LS_KEY = 'displayed_hashes_' + HASH_DIGEST_ALGO; const SEEN_HASH_LIST = new Set((localStorage.getItem(LS_KEY) || '').split(',')); const VIEWPORT_ENTRY_CHECKER = (entry) => { const TGT = entry.target; if (entry.isIntersecting) { // entered viewport if (MAP_EL_TIMEOUT.get(TGT)) { // already measuring - quick re-entry return } // measure time in viewport MAP_EL_TIMEOUT.set( TGT, window.setTimeout(processVisibleEntry, VIEWPORT_EXPOSITION_DURATION_UNTIL_READ) ); } else { // left viewport MAP_EL_TIMEOUT.delete(TGT); } function processVisibleEntry() { if (MAP_EL_TIMEOUT.get(TGT)) { // HA! STILL in viewport! // mark as read TGT.classList.remove(CSS_CLASSES.unread); TGT.classList.add(CSS_CLASSES.read); SEEN_HASH_LIST.add(MAP_EL_HASH.get(TGT)) MAP_EL_TIMEOUT.delete(TGT); // not interested in this element anymore VIEWPORT_OBSERVER.unobserve(TGT); // TODO move the persistence to window unload and/or blur event for fewer LS writes localStorage.setItem( LS_KEY, Array.from(SEEN_HASH_LIST).join(',') ); } } } // initialize single observer const VIEWPORT_OBSERVER = new IntersectionObserver( (entries, observer) => { entries.forEach(_ => VIEWPORT_ENTRY_CHECKER(_)); }, { root: null, rootMargin: "-9%", // TODO use computed "lines" height here instead? threshold: 0 } ); // compute hash, look into list and mark and observe "new" items ELS_TO_WATCH.forEach(async el => { const hash = await makeHash(el.textContent); if (SEEN_HASH_LIST.has(hash)) { el.classList.add(CSS_CLASSES.old); return; } el.classList.add(CSS_CLASSES.unread); MAP_EL_HASH.set(el, hash); VIEWPORT_OBSERVER.observe(el); }); // string to base64 hash digest using native Crypto API async function makeHash (input) { return btoa( String.fromCharCode.apply( null, new Uint8Array( await crypto.subtle.digest( HASH_DIGEST_ALGO, (new TextEncoder()).encode(input) ) ) ) ); };
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址