Subreddit tab icons

Replaces tab icons (favicons) on reddit with icons of subreddits.

当前为 2023-02-15 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Subreddit tab icons
// @description  Replaces tab icons (favicons) on reddit with icons of subreddits.
// @version      1.1
// @license      MIT
// @author       Andrei Rybak
// @match        https://www.reddit.com/r/*
// @match        https://new.reddit.com/r/*
// @match        https://old.reddit.com/r/*
// @icon         https://www.redditstatic.com/desktop2x/img/favicon/android-icon-192x192.png
// @namespace    http://tampermonkey.net/
// @grant        none
// ==/UserScript==

/*
 * Copyright (c) 2023 Andrei Rybak
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

(function() {
    'use strict';

    const LOG_PREFIX = '[reddit tab icons]';
    const DEBUG_ENABLED = false;
    function error(...toLog) {
        console.error(LOG_PREFIX, ...toLog);
    }
    function warn(...toLog) {
        console.warn(LOG_PREFIX, ...toLog);
    }
    function log(...toLog) {
        console.log(LOG_PREFIX, ...toLog);
    }
    function debug(...toLog) {
        console.debug(LOG_PREFIX, ...toLog);
    }

    /*
     * Delay to wait after an error until next attempt, in milliseconds.
     * Doubles after every error.
     */
    var delayMs = 1000;
    const SPECIAL_NAMES = ['all', 'friends', 'popular'];

    const srNameRegex = /https:[/][/](www|old|new)[.]reddit[.]com[/]r[/](\w+)/g;
    const match = srNameRegex.exec(document.location.href);
    if (!match[0]) {
        error(`Could not find subreddit URL in "${document.location.href}". Aborting.`);
        return;
    }
    const srName = match[2];
    if (SPECIAL_NAMES.includes(srName)) {
        log(`Detected special subreddit "${srName}". Aborting.`);
        return;
    }
    const srUrl = match[0];
    const srDataUrl = `${srUrl}/about.json`;

    function tryAgain(errorFn) {
        log(`Trying again after ${delayMs} ms...`);
        setTimeout(errorFn, delayMs);
        delayMs = delayMs * 2;
    }

    function setFavicon(url, errorFn) {
        const faviconNodes = document.querySelectorAll('link[rel="icon"]');
        if (!faviconNodes || faviconNodes.length == 0) {
            warn("Couldn't find favicon elements.");
            tryAgain(errorFn);
            return;
        }
        log(`Using URL = "${url}". Done.`);
        faviconNodes.forEach(node => {
            node.href = url;
        });
    }

    /*
     * For some reason .community_icon is partially HTML encoded.
     * Seems like a Reddit bug. Work around the bug by HTML decoding
     * the string.
     */
    function cleanUpCommunityIcon(url) {
        if (!url || url.length == 0) {
            return url;
        }
        // https://stackoverflow.com/a/34064434/1083697
        function htmlDecode(input) {
            const doc = new DOMParser().parseFromString(input, "text/html");
            return doc.documentElement.textContent;
        }
        const res = htmlDecode(url);
        log(`Converted community_icon from "${url}" to "${res}".`);
        return res;
    }

    /*
     * I couldn't figure out a simple way to _reliably_ determine
     * if a website on www.reddit.com is Old Reddit or New Reddit.
     * So that's why there is this weird ping-pong error handling.
     */
    function replaceFavicon() {
        /*
         * For old.reddit.com
         */
        function replaceFaviconOld() {
            function useSrData(data) {
                if (DEBUG_ENABLED) {
                    debug('Received JSON:', data);
                }
                /*
                 * Not every subreddit has all these different images
                 * defined in their style/theme/look-and-feel/whatever.
                 * Therefore, try several different options.
                 */
                const communityIcon = cleanUpCommunityIcon(data.community_icon);
                const options = [communityIcon, data.icon_img, data.header_img];
                for (const img of options) {
                    if (img && img.length > 0) {
                        setFavicon(img, replaceFaviconNew);
                        return;
                    }
                }
                /*
                 * If we loaded "about.json" and it come up empty for
                 * all three options of different fields for the icon,
                 * it means that the subreddit likely doesn't have
                 * the icon defined in the settings at all. Abort in
                 * such cases.
                 */
                warn(`It seems that subreddit "${srName}" doesn't have its own icons defined. Aborting.`);
            }
            /*
             * Download data about the subreddit from Reddit API.
             * https://old.reddit.com/r/redditdev/comments/dot8tn/how_can_i_get_the_icon_of_a_subreddit/
             */
            log(`Loading from "${srDataUrl}"...`);
            const srDataPromise = fetch(srDataUrl);
            // https://stackoverflow.com/a/43175774/1083697
            srDataPromise.then(res => res.json())
                .then(json => useSrData(json.data))
                .catch(err => {
                error(`Got error while getting ${srDataUrl}`, err);
                tryAgain(replaceFaviconNew);
            });
        }

        /*
         * For new.reddit.com
         */
        function replaceFaviconNew() {
            const srIcon = document.querySelector('img[alt="Subreddit-Symbol"]');
            if (!srIcon) {
                warn("Couldn't find the icon in HTML of New Reddit.");
                tryAgain(replaceFaviconOld);
                return;
            }
            setFavicon(srIcon.src, replaceFaviconOld);
        }

        /*
         * Users of Old Reddit are more likely to use explicit URL
         * old.reddit.com than users of New Reddit, so check for
         * "old" first.
         */
        if (document.location.hostname.includes('old')) {
            replaceFaviconOld();
        } else {
            /*
             * Here the user is either using www.reddit.com or
             * explicit new.reddit.com. Users of www.reddit.com are
             * more likely to be using New Reddit (i.e. the default),
             * so try it out first.
             */
            replaceFaviconNew();
        }
    }

    replaceFavicon();
})();