Hackernews Modern

Improved mobile usability and modern styling for Hackernews

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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();
        }
    }
}());