Improved mobile usability and modern styling for Hackernews
// ==UserScript== // @name Hackernews Modern // @namespace sagiegurari // @version 1.9 // @author Sagie Gur-Ari // @description Improved mobile usability and modern styling for Hackernews // @homepage https://github.com/sagiegurari/userscripts-hackernews // @supportURL https://github.com/sagiegurari/userscripts-hackernews/issues // @match https://news.ycombinator.com/* // @match https://hckrnews.com/* // @match https://hackerweb.app/* // @grant none // @license MIT License // ==/UserScript== (function run() { 'use strict'; const isAndroid = navigator.userAgent.toLowerCase('android') !== -1; const isEmulator = isAndroid && !navigator.userAgentData.mobile; const mobileOrEmulator = isEmulator || navigator.userAgentData.mobile; const isDebug = isEmulator; const logDebug = param => { console.log('[DEBUG]', param); }; const element = document.createElement('style'); element.type = 'text/css'; document.head.appendChild(element); const styleSheet = element.sheet; const ycombinatorDomain = window.location.hostname.indexOf('.ycombinator.com') !== -1; const hckrnewsDomain = !ycombinatorDomain && window.location.hostname.indexOf('hckrnews.com') !== -1; const hackerwebDomain = !ycombinatorDomain && window.location.hostname.indexOf('hackerweb.app') !== -1; const articlePage = (ycombinatorDomain && window.location.search.indexOf('id=') !== -1) || (hackerwebDomain && window.location.href.indexOf('/#/item/') !== -1); logDebug({ platform: { isAndroid, isEmulator, mobileOrEmulator }, isDebug, articlePage, domain: { ycombinatorDomain, hckrnewsDomain, hackerwebDomain } }); const addRules = (rules) => { rules.forEach(cssRule => { styleSheet.insertRule(cssRule, styleSheet.cssRules.length); }); }; const cssRules = [ // defaults '.subtext .age a[href^="item"] { color: #828282; }', // colors '#hnmain tr:first-child td, .comment-tree { background-color: #333; }', 'html, body, #hnmain, #hnmain table.itemList tr:first-child td { background-color: #222; }', 'a:link, .subtext a[href^="item"]:not(:visited), a:link.togg.clicky, .commtext, .comment-tree a[rel="nofollow"], .comment-tree .reply a { color: #eee; }', '.visited a.titlelink, .visited a:link, .visited .subtext a[href^="item"] { color: #888; }', '.morelink { text-align: center; display: block; margin: 10px 40px 10px 0; background-color: #af4000; font-weight: bold; padding: 10px; }', ]; // if mobile or emulator if (mobileOrEmulator) { cssRules.push(...[ // styles '.pagetop { font-size: 16pt; }', '.title { font-size: 14pt; }', '.comhead { font-size: 12pt; }', '.subtext { font-size: 0; padding: 5px 0; }', '.subtext span { padding: 0 2px; }', '.subtext span, .subtext a:not([href^="item"]), .subtext .age a[href^="item"] { font-size: 12pt; text-decoration: none; }', '.subtext a[href^="item"] { font-size: 14pt; text-decoration: underline; }', '.subtext a[href^="hide"] { display: none; }', '.default { font-size: 12pt }', ]); } if (hckrnewsDomain) { cssRules.push(...[ 'body, a:hover, a, .points, .comments { color: #eee; }', 'body .entries a:hover, body .nav > li > a:hover { background-color: #333; }', '.form-actions { background-color: #222 }', ]); } else if (hackerwebDomain && articlePage) { cssRules.push(...[ '.view > header, body header, body .grouped-tableview, .post-content, body .view.view.view section { background-color: #333; }', 'body .view .post-content pre, body .view section.comments pre { background-color: #222 }', 'body .view .post-content header h1, body p, body pre, .view section.comments button.comments-toggle, body li { color: #eee; }', '.view section.comments button.comments-toggle, .view section.comments button.comments-toggle:hover { background-color: #555 }', ]); } addRules(cssRules); if (articlePage) { // collapse non top comments document.querySelectorAll('.ind:not([indent="0"])').forEach(topCommentIndent => { topCommentIndent.parentElement.querySelectorAll('.togg.clicky').forEach(toggle => toggle.click()); }); // remove root/next/prev links addRules([ '.navs .clicky:not(.togg) { display: none; }', ]); } else { const storage = window.localStorage; if (storage && typeof storage.getItem === 'function') { const KEY = 'hn-cache-visited'; const CACHE_LIMIT = 1000; const readFromCache = () => { const listStr = storage.getItem(KEY); if (!listStr) { return []; } return listStr.split(','); }; const writeToCache = (ids) => { if (!ids || !Array.isArray(ids) || !ids.length) { return; } // add to start cache.unshift(...ids); // remove duplicates const seen = {}; cache = cache.filter(function (item) { if (seen[item]) { return false; } seen[item] = true; return true; }); // trim const extraCount = cache.length - CACHE_LIMIT; if (extraCount) { cache.splice(cache.length - extraCount, extraCount); } storage.setItem(KEY, cache.join(',')); }; let cache = readFromCache(); const entryRowSelector = ycombinatorDomain ? 'tr.athing' : '.entry.row'; const linkSelector = ycombinatorDomain ? 'tr.visited + tr' : '.entry.row .link.story'; // mark visited const markVisited = () => { document.querySelectorAll(entryRowSelector).forEach(element => { if (cache.indexOf(element.id) !== -1) { element.classList.add('visited'); } }); document.querySelectorAll(linkSelector).forEach(element => { element.classList.add('visited'); }); }; markVisited(); // listen to scroll and add to cache const markVisibleAsVisited = () => { const elements = document.querySelectorAll(`${entryRowSelector}:not(.visited)`); let started = false; const ids = []; for (let index = 0; index < elements.length; index++) { const element = elements[index]; const bounding = element.getBoundingClientRect(); if (bounding.top >= 0 && bounding.bottom <= window.innerHeight) { started = true; ids.push(element.id); } else if (started) { break; } } if (ids.length) { writeToCache(ids); } }; let timeoutID = null; document.addEventListener('scroll', () => { clearTimeout(timeoutID); timeoutID = setTimeout(markVisibleAsVisited, 25); }, { passive: true }); markVisibleAsVisited(); } } }());