Hackernews Modern

Improved mobile usability and modern styling for Hackernews

目前为 2022-08-29 提交的版本。查看 最新版本

// ==UserScript==
// @name         Hackernews Modern
// @namespace    sagiegurari
// @version      1.4
// @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/*
// @grant        none
// @license      MIT License
// ==/UserScript==

(function run() {
    'use strict';

    const element = document.createElement('style');
    element.type = 'text/css';
    document.head.appendChild(element);
    const styleSheet = element.sheet;

    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) { color: #eee; }',
        '.commtext, .comment-tree a[rel="nofollow"], .comment-tree .reply a { color: #eee; }',
        '.visited a.titlelink { color: #888; }',
    ];

    // if mobile or emulator
    if (navigator.userAgent.toLowerCase('android') !== -1 || navigator.userAgentData.mobile) {
        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 }',
        ]);
    }

    cssRules.forEach(cssRule => {
        styleSheet.insertRule(cssRule, styleSheet.cssRules.length);
    });

    // collapse non top comments
    document.querySelectorAll('.ind:not([indent="0"])').forEach(topCommentIndent => {
        topCommentIndent.parentElement.querySelectorAll('.togg.clicky').forEach(toggle => toggle.click());
    });

    const storage = window.localStorage;
    if (storage && typeof storage.getItem === 'function') {
        const KEY = 'hn-cache-visited';
        const CACHE_LIMIT = 100;

        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();

        // mark visited
        const markVisited = () => {
            const elements = document.querySelectorAll('tr.athing');

            for (let index = 0; index < elements.length; index++) {
                const element = elements[index];
                if (cache.indexOf(element.id) !== -1) {
                    element.classList.add('visited');
                }
            }
        };
        markVisited();

        // listen to scroll and add to cache
        const markVisibleAsVisited = () => {
            const elements = document.querySelectorAll('tr.athing: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();
    }
}());