Reddit Chemo

Minimizes unwanted subreddit posts (of your choosing by selecting those to ban) and removes ads on the Reddit feed.

当前为 2022-08-18 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name        Reddit Chemo
// @namespace   https://lawrenzo.com/p/reddit-chemo
// @version     2.1.1
// @description Minimizes unwanted subreddit posts (of your choosing by selecting those to ban) and removes ads on the Reddit feed.
// @author      Lawrence Sim
// @license     WTFPL (http://www.wtfpl.net)
// @grant       unsafeWindow
// @grant       GM_getValue
// @grant       GM.getValue
// @grant       GM_setValue
// @grant       GM.setValue
// @match       *://*.reddit.com/*
// ==/UserScript==
(function() {
    //-------------------------------------------------------------------------------------
    // settings
    //-------------------------------------------------------------------------------------
    var darkMode = true;
    //-------------------------------------------------------------------------------------
    // Prepare styles
    //-------------------------------------------------------------------------------------
    let styles = {
        ".rchemo-post": {
            background:        "rgb(255 240 240)"
        },
        ".rchemo-dark .rchemo-post": {
            background:        "rgb(80 70 70)"
        },
        ".rchemo-post.rchemo-card": {
            height:            "2em"
        },
        ".rchemo-post div[data-click-id='background']": {
            background:        "none !important",
            color:             "rgb(163 149 149)",
            'font-size':       "0.7em",
            height:            "1.6em",
            'line-height':     "1.7em"
        },
        ".rchemo-post.rchemo-classic div[data-click-id='background']": {
            padding:           "0.1em 0 0.15em 0.7em"
        },
        ".rchemo-post.rchemo-compact div[data-click-id='background']": {
            padding:           "0",
            "font-size":       "0.6em"
        },
        ".rchemo-post.rchemo-card div[data-click-id='background']": {
            'border-left':     "none",
            padding:           "0.2em 0.4em",
            "font-size":       "0.95em"
        },
        ".rchemo-post button[data-click-id='downvote'] .icon": {
            top:               "2px",
            left:              "2px",
            'line-height':     "14px",
            'font-size':       "14px"
        },
        ".rchemo-post.rchemo-classic .voteButton, .rchemo-post.rchemo-classic .voteButton span": {
            width:             "46px"
        },
        ".rchemo-post.rchemo-card button[data-click-id='downvote'] .icon": {
            top:               "8px",
            left:              "2px",
            'line-height':     "20px",
            'font-size':       "20px"
        },
        ".rchemo-post.rchemo-card .voteButton, .rchemo-post.rchemo-card .voteButton span": {
            width:             "46px",
            height:            "36px"
        },
        ".rchemo-unban": {
            margin:            "0",
            'margin-left':     "0.6em",
            padding:           "0.1em 0.4em",
            background:        "rgb(135 158 200)",
            'border-radius':   "2px",
            color:             "#fff",
            'line-height':     "normal"
        },
        ".rchemo-dark .rchemo-unban": {
            background:        "rgb(145 125 125)",
            color:             "#000"
        },
        ".rchemo-post.rchemo-compact .rchemo-unban": {
            padding:           "0 0.4em",
            'line-height':     "1.2em"
        },
        ".rchemo-post.rchemo-card .rchemo-unban": {
            'font-size':      "0.85em"
        },
        ".rchemo-unban:hover": {
            background:        "rgb(185 200 220)"
        },
        ".rchemo-dark .rchemo-unban:hover": {
            background:        "rgb(205 180 180)"
        },
        ".rchemo-join, .rchemo-ban": {
            'border-radius':   "0.9em"
        },
        ".rchemo-classic .rchemo-join, .rchemo-compact .rchemo-join": {
            'min-height':      "auto",
            padding:           "0.3em 0.6em",
            'line-height':     "0.8em",
            'font-size':       "0.7em"
        },
        ".rchemo-ban": {
            margin:            "0 0.2em 0 0.4em",
            background:        "rgb(120 45 45)",
            padding:           "0.34em 1.2em",
            color:             "#fff",
            'line-height':     "1.2em",
            'font-size':       "1.06em"
        },
        ".rchemo-ban:hover": {
            background:      "rgb(180 85 85)"
        },
        ".rchemo-classic .rchemo-ban, .rchemo-compact .rchemo-ban": {
            padding:           "0.2em 0.6em",
            'line-height':     "0.8em",
            'font-size':       "0.75em"
        },
        ".rchemo-compact .rchemo-ban": {
            'font-size':       "0.9em"
        },
        ".rchemo-counter": {
            position:          "fixed",
            top:               "60px",
            right:             "25px",
            width:             "120px",
            "z-index":         "70",
            background:        "rgb(255 240 240)",
            border:            "1px solid rgb(123, 106, 109)",
            "border-radius":   "0.2em",
            padding:           "0.2em 0.4em",
            "text-align":      "center",
            "font-size":       "0.75em",
            color:             "#333"
        },
        ".rchemo-counter .rchemo-minimize": {
            position:          "absolute",
            top:               "-1px",
            right:             "6px",
            "font-size":       "1.3em",
            "text-decoration": "none"
        },
        ".rchemo-dark .rchemo-counter": {
            background:        "rgb(80 70 70)",
            border:            "1px solid rgb(123, 106, 109)",
            color:             "#ddd"
        },
        ".rchemo-counter-content": {
            "margin-top":      "0.4em"
        },
        ".rchemo-counter p": {
            padding:           "0",
            margin:            "0"
        },
        ".rchemo-counter button": {
            "margin-bottom":   "0.2em"
        },
        ".rchemo-counter .rchemo-btn-showhide": {
            "margin-top":      "0.2em",
            "text-decoration": "underline",
            color:             "rgb(77, 113, 68)"
        },
        ".rchemo-dark .rchemo-counter .rchemo-btn-showhide": {
            color:             "rgb(182, 198, 178)"
        },
        ".rchemo-counter .rchemo-btn-showhide:hover": {
            color:             "rgb(141, 187, 128) !important"
        },
        ".rchemo-counter .rchemo-btn-darkmode": {
            "margin-top":      "0.2em",
            background:        "rgb(135, 158, 200)",
            color:             "#eee",
            padding:           "0.2em 0.5em",
            "border-radius":   "0.6em"
        },
        ".rchemo-dark .rchemo-counter .rchemo-btn-darkmode": {
            background:        "rgb(0 0 0)",
            color:             "#aaa"
        }
    };
    let styletxt = "";
    for(let selector in styles) {
        styletxt += `${selector} {`;
        for(let skey in styles[selector]) {
            styletxt += `${skey}: ${styles[selector][skey]};`;
        }
        styletxt += "}";
    }
    let styleElem = document.createElement('style');
    styleElem.className = 'rchemo-styles';
    styleElem.innerText = styletxt;
    document.body.appendChild(styleElem);
    var cssRules = Array.from(document.styleSheets[document.styleSheets.length-1].cssRules);
    //-------------------------------------------------------------------------------------
    // handlers for banned list
    //-------------------------------------------------------------------------------------
    GM_getValue = GM_getValue || GM.getValue;
    GM_setValue = GM_setValue || GM.setValue;
    var banSubreddits = (
        (GM_getValue("banned") && GM_getValue("banned").split("|"))
        || window.bannedSubreddits
        || (unsafeWindow && unsafeWindow.bannedSubreddits)
        || []
    );
    banSubreddits = banSubreddits.map(n => n.trim().toLowerCase())
                                 .map(n => n.startsWith("r/") ? n : `r/${n}`);
    function addBanned(subreddit) {
        banSubreddits.push(subreddit.trim().toLowerCase().startsWith("r/") ? subreddit : `r/${subreddit}`);
        GM_setValue("banned", banSubreddits.join("|"));
    }
    function removeBanned(subreddit) {
        subreddit = subreddit.startsWith("r/") ? subreddit : `r/${subreddit}`;
        let index = banSubreddits.indexOf(subreddit);
        if(~index) {
            banSubreddits.splice(index, 1);
            GM_setValue("banned", banSubreddits.join("|"));
        }
    }
    //-------------------------------------------------------------------------------------
    // control element and options
    //-------------------------------------------------------------------------------------
    var counterElem = document.createElement('div');
    counterElem.className = 'rchemo-counter';
    counterElem.style.display = "none";
    counterElem.innerHTML = (
        "<p style='font-weight:bold'>Reddit Chemo</p>" +
        "<div class='rchemo-counter-content'>" +
          "<p>Posts blocked: <span class='rchemo-count'>0</span></p>" +
          "<button class='rchemo-btn-showhide'></button>" +
          "<button class='rchemo-btn-darkmode'>light mode</button>" +
        "</div>" +
        "<button class='rchemo-minimize'></button>"
    );
    document.body.appendChild(counterElem);

    var countElem = counterElem.querySelector(".rchemo-count"),
        blockCount = 0;
    function showCounter() { counterElem.style.display = ""; }
    function hideCounter() { counterElem.style.display = "none"; }
    function resetCount() {
        countElem.innerHTML = blockCount = 0;
        hideCounter();
    }
    function incrementCount() {
        countElem.innerHTML = ++blockCount;
        showCounter();
    }

    var minimizeBtn = counterElem.querySelector(".rchemo-minimize"),
        contentElem = counterElem.querySelector(".rchemo-counter-content"),
        minControl  = GM_getValue("minimized");
    if(minControl === null || typeof minControl === "undefined") minControl = true;
    function setMinimized(isMinimized) {
        minControl = isMinimized;
        GM_setValue("minimized", minControl);
        contentElem.style.display = minControl ? "none" : "";
        minimizeBtn.innerHTML = minControl ? "+" : "–";
    }
    setMinimized(minControl);
    minimizeBtn.addEventListener('click', () => setMinimized(!minControl));

    var showHideBtn = counterElem.querySelector(".rchemo-btn-showhide"),
        postCssRule = cssRules.filter(r => r.selectorText == ".rchemo-post")[0],
        showBanned  = GM_getValue("showbanned");
    if(showBanned === null || typeof showBanned === "undefined") showBanned = true;
    function setShowBanned(visible) {
        showBanned = !!visible;
        GM_setValue("showbanned", showBanned);
        showHideBtn.innerHTML = (showBanned ? "Hide" : "Show") + " blocked posts";
        postCssRule.style.display = showBanned ? "" : "none";
    };
    setShowBanned(showBanned);
    showHideBtn.addEventListener('click', () => setShowBanned(!showBanned));

    var darkBtn = counterElem.querySelector(".rchemo-btn-darkmode"),
        darkmode = GM_getValue("darkmode");
    function setDarkMode(darkOn) {
        darkmode = !!darkOn;
        GM_setValue("darkmode", darkOn);
        if(darkmode) {
            document.body.classList.add("rchemo-dark");
            darkBtn.innerHTML = "dark mode";
        } else {
            document.body.classList.remove("rchemo-dark");
            darkBtn.innerHTML = "light mode";
        }
    }
    setDarkMode(darkmode);
    darkBtn.addEventListener('click', () => setDarkMode(!darkmode));
    //-------------------------------------------------------------------------------------
    // block post function
    //-------------------------------------------------------------------------------------
    function blockPost(post, sub, mode) {
        post.classList.add("rchemo-post");
        if(mode == 'compact') post = post.children[0];
        let child, voteElem, subelm, icon;
        Array.from(post.children).forEach(child => {
            if(child.getAttribute("data-click-id") === "background") {
                child.innerHTML = `Post from ${sub} removed`;
                let rmvBtn = document.createElement("button");
                rmvBtn.innerHTML = `Remove ban`;
                rmvBtn.classList.add("rchemo-unban");
                rmvBtn.addEventListener('click', function() {
                    this.parentNode.innerHTML = `Post from ${sub} removed from banned list (refresh for reload)`;
                    this.remove();
                    removeBanned(sub);
                });
                child.append(rmvBtn);
                return;
            }
            let downvote = child.querySelectorAll("#vote-arrows-"+post.id + " button[data-click-id='downvote']");
            if(downvote && downvote.length) {
                child.style.top = "-0.7em";
                for(let j = 0; j < downvote.length; ++j) {
                    let voteElem = downvote[j].parentNode;
                    voteElem.style.margin = 0;
                    voteElem.style.padding = 0;
                    voteElem.parentNode.style.border = "none";
                    voteElem.innerHTML = "";
                    voteElem.append(downvote[j]);
                }
                return;
            }
            child.remove()
        });
        incrementCount();
    }
    //-------------------------------------------------------------------------------------
    // watchers as React will sometimes restore/readd posts
    //-------------------------------------------------------------------------------------
    var emptyObserver = new MutationObserver(mutated => {
        mutated.forEach(mutant => {
            if(mutant.target.children.length) {
                mutant.target.innerHTML = "";
                mutant.target.style.border = "none";
                mutant.target.style.fill = "none";
            }
        });
    });
    var blockObserver = new MutationObserver(mutated => {
        mutated.forEach(mutant => {
            if(mutant.target.children.length) {
                let post = mutant.target.closest("div[data-testid='post-container']");
                blockPost(post, post.getAttribute("chemo"));
            }
        });
    });
    function refreshObservers() {
        emptyObserver.disconnect();
        blockObserver.disconnect();
    };
    //-------------------------------------------------------------------------------------
    // resolvers for when post data is not yet loaded
    //-------------------------------------------------------------------------------------
    function watchPost(post) {
        (new MutationObserver((mutated, observer) => {
            if(checkAd(post)) return observer.disconnect();
            let subreddit = getSubredditNode(post);
            if(checkBanned(post, subreddit)) return observer.disconnect();
        })).observe(post, {childList:true, subtree:true});
    }
    function watchSubreddit(post, subreddit) {
        (new MutationObserver((mutated, observer) => {
            if(checkBanned(post, subreddit)) observer.disconnect();
        })).observe(subreddit, {childList:true, attributes:true});
    }
    //-------------------------------------------------------------------------------------
    // process/check post functions
    //-------------------------------------------------------------------------------------
    function checkAd(post) {
        if(
            Array.from(post.querySelectorAll("span"))
                 .find(span => span.innerText && span.innerText.toLowerCase() === "promoted")
        ) {
            post.innerHTML = "";
            post.style.border = "none";
            post.style.fill = "none";
            console.log("Ad removed.");
            emptyObserver.observe(post, {childList:true});
            return 1;
        }
        return 0;
    }
    function checkBanned(post, subreddit) {
        let mode = 'classic';
        if(post.children.length === 1) {
            mode = 'compact';
            post.classList.add("rchemo-compact");
        } else if(post.querySelectorAll("a[data-click-id='subreddit']").length > 1) {
            mode = 'card';
            post.classList.add("rchemo-card");
        } else {
            post.classList.add("rchemo-classic");
        }
        if(subreddit && subreddit.innerText) {
            let subname = subreddit.innerText.toLowerCase();
            if(~banSubreddits.indexOf(subname)) {
                post.setAttribute("chemo", subreddit.innerText);
                blockPost(post, subname, mode);
                console.log(`Banned subreddit (${subname}) post removed.`);
                blockObserver.observe(
                    post.querySelector("div[data-click-id='background']"),
                    {childList:true}
                );
                return 1;
            }
            let addBtn = document.createElement("button");
            addBtn.innerHTML = `Ban`;
            addBtn.classList.add("rchemo-ban");
            addBtn.addEventListener('click', function() {
                this.remove();
                addBanned(subname);
                blockPost(post, subname, mode);
            });
            let subscribeBtn = post.querySelector("#subscribe-button-"+post.id);
            if(subscribeBtn) {
                subscribeBtn.after(addBtn);
                subscribeBtn.classList.add("rchemo-join");
            } else if(mode == 'compact') {
                subreddit.after(addBtn);
            }
            return -1;
        }
        return 0;
    }
    function getSubredditNode(post) {
        let subreddit = post.querySelectorAll("a[data-click-id='subreddit']");
        if(!subreddit || !subreddit.length) return null;
        for(let i = 0; i < subreddit.length; ++i) {
            // card layout has two subreddit click elements, one with icon/image
            if(!subreddit[i].children.length) return subreddit[i];
        }
    }
    function processNodes(nodes) {
        if(!nodes || !nodes.length) return;
        nodes.forEach(node => {
            if(!node || !node.querySelectorAll) return;
            node.querySelectorAll("div[data-testid='post-container']").forEach(post => {
                if(post.getAttribute("chemo")) return;
                post.setAttribute("chemo", 1);
                if(checkAd(post)) return;
                let subreddit = getSubredditNode(post);
                if(!subreddit) return watchPost(post);
                if(!checkBanned(post, subreddit)) watchSubreddit(post, subreddit);
            });
        });
    }
    //-------------------------------------------------------------------------------------
    // init
    //-------------------------------------------------------------------------------------
    processNodes([document.querySelector(".ListingLayout-outerContainer")]);
    //-------------------------------------------------------------------------------------
    // if Reddit Watcher available, use it for update/change hooks
    //-------------------------------------------------------------------------------------
    let redditWatcher = window.redditWatcher || (unsafeWindow && unsafeWindow.redditWatcher);
    if(redditWatcher) {
        redditWatcher.feed.onChange(feed => {
            resetCount();
            refreshObservers();
            processNodes([feed]);
        });
        redditWatcher.feed.onUpdate((feed, mutated) => {
            mutated && mutated.forEach(mutant => processNodes(mutant.addedNodes));
        });
        return;
    }
    //-------------------------------------------------------------------------------------
    // otherwise manually create watcher
    //-------------------------------------------------------------------------------------
    function getFeedWrapper() {
        let listingLayout = document.querySelector(".ListingLayout-outerContainer"),
            firstPost     = listingLayout && listingLayout.querySelector("div[data-testid='post-container']"),
            feedWrapper   = firstPost && firstPost.parentNode;
        while(feedWrapper && !feedWrapper.nextSibling) {
            if(feedWrapper == listingLayout) return null;
            feedWrapper = feedWrapper.parentNode || null;
        }
        return feedWrapper && feedWrapper.parentNode;
    }
    var feedWatcher = new MutationObserver(mutated => mutated.forEach(mutant => processNodes(mutant.addedNodes))),
        lastFeedWrapper = null;
    (new MutationObserver(() => {
        let feedWrapper = getFeedWrapper();
        if(feedWrapper !== lastFeedWrapper) {
            resetCount();
            refreshObservers();
            feedWatcher.disconnect();
            if(feedWrapper) {
                processNodes([feedWrapper]);
                feedWatcher.observe(feedWrapper, {childList:true});
                lastFeedWrapper = feedWrapper;
            }
        }
    })).observe(document.body, {childList:true, subtree:true});
})();