Hackernews Modern

Improved mobile usability and modern styling for Hackernews

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Для установки этого скрипта вам необходимо установить расширение, такое как Tampermonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или 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();
        }
    }
}());