redditmod

Subset of RES features I like.

目前为 2017-05-13 提交的版本。查看 最新版本

// ==UserScript==
// @name        redditmod
// @namespace   derv82
// @description Subset of RES features I like.
// @include     https://*.reddit.com/*
// @version     1.4
// @grant       GM_xmlhttpRequest
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_addStyle
// ==/UserScript==

/*
TODO:

* TODO Features:
-> Load Twitch.tv clips
-> Viewing a post's media should mark the link as "visited" (purple).
-> Clicking "X comments" loads comments in a dropdown
-> Posts that link to other posts shows comments in dropdown, see https://www.reddit.com/r/SubredditSimMeta/ for examples
-> UX Overhaul: Hovering a post has /large/ buttons to 1. Expand the content, 2. Expand the comments, 3. Hide this post, 4. Filter the subreddit
-> Media overhaul: Don't rely on max-height, allow resizing of images
-> Customizable UI colors. Or at least different themes.
-> CSS overhaul: Define colors as variables. call GM_addStyle for different styles.
*/

var Redditmod = {};

Redditmod.CSS = (function() {
    var self = this;
    this.colors = {
        bg: {
            base: "#121234",
            input: "#232356",
            filterHover: "#f00",
            flair: "#18f",
            thingHover: "#18f"
        },
        fg: {
            text: "#aaa",
            title: "#18f",
            visited: "#88f",
            filter: "#f00",
            filterHover: "#fff",
            flair: "#000",
            spoiler: "#f80"
        },
        border: {
            input: "#555",
            flair: "#000"
        },
        shadow: { thingHover: "#18f" }
    };
    this.selectorsAndStyles = [{
        selectors: [
            "body", ".side", "#header", "#sr-header-area", "#header-bottom-right", ".drop-choices",
            ".tabmenu li.selected a", ".link .usertext-body .md", ".morelink", ".morelink .nub", ".linkinfo",
            ".server-seconds", ".trophy-area .content"
        ],
        styles: {"background-color": this.colors.bg.base}
    }, {
        selectors: [".tabmenu li a", "input", "textarea", ".infobar", ".reddit-infobar"],
        styles: {"background-color": this.colors.bg.input}
    }, {
        selectors: [".morelink", ".morelink .nub"],
        styles: {"background-image":"none"}
    }, {
        selectors: [
            ".pagename a", ".pagename.selected", ".sr-bar a", ".dropdown.srdrop .selected", ".md", "h1", "h2", "h3", "h4", "h5", "h6",
            ".eddit-content", "input", "textarea", ".titlebox h1 a", ".side", ".lightdrop", ".content"
        ],
        styles: {color: this.colors.fg.text}
    }, {
        selectors: [".thing .title", ".tagline a"],
        styles: {color: this.colors.fg.title}
    }, {
        selectors: [".thing a.title:visited"],
        styles: {color: this.colors.fg.visited} // + " !important"}
    }, {
        selectors: [".tabmenu li.selected a"],
        styles: {"border-bottom-color": this.colors.bg.base}
    }, {
        selectors: [".pagename"],
        styles: {
            position:"relative !important",
            bottom: "0px !important"
        }
    }, {
        selectors: ["a.thumbnail.self", "a.thumbnail.default"],
        styles: {visibility: "hidden"}
    }, {
        selectors: [".midcol"],
        styles: {"margin-left": "0px"}
    }, {
        selectors: ["input", "textarea", "button"],
        styles: {border: "solid 0.5px " + this.colors.border.input}
    }, {
        selectors: [".side"],
        styles: {
            position: "absolute",
            right: "0px",
            "z-index": "1111",
        }
    }, {
        selectors: [".side", "#header-bottom-right"],
        styles: {
            opacity: "0.0",
            transition: "opacity 0.3s linear"
        }
    }, {
        selectors: [".side:hover", "#header-bottom-right:hover"],
        styles: { opacity: "1.0" }
    }, {
        selectors: [".eddit-filter-subreddit-link"],
        styles:  {
            color: this.colors.fg.filter + " !important",
            "border-radius": "5px",
            padding: "0 0.1rem 0 0.1rem",
            "font-size": "0.5rem",
            border: "solid 1px " + this.colors.fg.filter + " !important",
            "margin-left": "0.2rem"
        }
    }, {
        selectors: [".eddit-filter-subreddit-link:hover"],
        styles: {
            "background-color": this.colors.bg.filterHover + " !important",
            color: this.colors.fg.filterHover + " !important",
           "text-decoration": "none !important"
        }
    }, {
        selectors: [".eddit-content-other"],
        styles: {
            color: this.colors.fg.text,
            display: "block",
            width: "100%",
            height: "100%"
        }
    }, {
        selectors: ["#siteTable .thing.link", ".thing.comment"],
        styles: {
            cursor: "pointer",
            padding: "5px",
            "border-radius": "10px"
        }
    }, {
        selectors: ["#siteTable .thing.link:hover", ".eddit-comment-hover"],
        styles: {"box-shadow": "0px 0px 5px " + this.colors.shadow.thingHover}
    }, {
        selectors: [".linkflairlabel"],
        styles: {
            border: "solid 0.5px " + this.colors.border.flair,
            "background-color": this.colors.bg.flair,
            color: this.colors.fg.flair
        }
    }, {
        selectors: [".spoiler-stamp"],
        styles: {
            "color": this.colors.fg.spoiler,
            "border-color": this.colors.fg.spoiler
        }
    }, {
        selectors: ["a:hover"],
        styles: {"text-decoration": "underline"}
    }, {
        selectors: [
            ".organic-listing", ".listing-chooser", "#sr-more-link", "#header-img", ".rank", "li.share",
            "li.give-gold-button", ".footer-parent", ".eddit-duplicate", "#sr-bar", ".sr-list > ul:nth-child(3n)",
            ".sr-list > .separator", ".filtered-details", ".titlebox.rounded", ".domain", //".expando-button",
            ".goldvertisement", ".titlebox form.toggle", ".side .tagline", ".eddit-filtered-post"
        ],
        styles: {display: "none !important"}
    }];
    this.cssText = function() {
        return self.selectorsAndStyles.map(function(ss) {
            var styles = "", key;
            for (key in ss.styles) {
                if (styles !== "") styles += ";";
                styles += key + ":" + ss.styles[key];
            }
            return ss.selectors.join(",") + "{" + styles + "}";
        }).join("");
    };
    GM_addStyle(this.cssText());
    return this;
})();

Redditmod.Error = function(message, url) {
    var div = document.createElement("div");
    // TODO: Move style to stylesheet.
    div.style = "background-color: #800; border: solid 0.5px #c00; color: #fff; font-weight: bold; font-size: 1.2rem; padding: 5px;";
    div.classList.add("eddit-content-error");
    div.textContent = message;
    if (url) {
        // TODO: Move style to stylesheet.
        div.innerHTML += '<a href="' + url + '" target="_BLANK" style="color: #fff">' + url + '</a>';
    }
    return div;
};

Redditmod.ImagePromise = function(sourceURLs) {
    if (!(this instanceof Redditmod.ImagePromise)) return new Redditmod.ImagePromise(sourceURLs);
    var self = this;
    this.sourceURLs = (sourceURLs instanceof String || typeof(sourceURLs) === "string") ? [sourceURLs] : sourceURLs;
    this.currentIndex = 0;
    this.img = null;

    this.createAlbumNav = function() {
        var albumStatus = document.createElement("span");
        var albumPrevButton = document.createElement("a");
        albumPrevButton.textContent = "<";
        albumPrevButton.style = "cursor: pointer; font-size: 1.4rem;";
        albumPrevButton.addEventListener("click", function(e) {
            e.stopPropagation();
            if (self.currentIndex === 0) {
                self.currentIndex = index = self.sourceURLs.length;
            }
            self.currentIndex--;
            albumStatus.textContent = (self.currentIndex + 1) + "/" + self.sourceURLs.length;
            self.img.src = self.sourceURLs[self.currentIndex];
        }, true);

        var albumNextButton  = document.createElement("a");
        albumNextButton.textContent = ">";
        albumNextButton.style = "cursor: pointer; font-size: 1.4rem;";
        albumNextButton.addEventListener("click", function(e) {
            e.stopPropagation();
            if (self.currentIndex === self.sourceURLs.length - 1) {
                self.currentIndex = -1;
            }
            self.currentIndex++;
            albumStatus.textContent = (self.currentIndex + 1) + "/" + self.sourceURLs.length;
            self.img.src = self.sourceURLs[self.currentIndex];
        }, true);

        albumStatus.textContent = "1/" + self.sourceURLs.length;
        albumStatus.style = "cursor: default; font-size: 1.4rem;";

        var albumNav = document.createElement("div");
        albumNav.appendChild(albumPrevButton);
        albumNav.appendChild(albumStatus);
        albumNav.appendChild(albumNextButton);
        return albumNav;
    };

    return new Promise(function(resolve, reject) {
        var imageContainer = document.createElement("div");
        if (self.sourceURLs.length > 1) {
            imageContainer.appendChild(self.createAlbumNav());
        }

        self.img = document.createElement("img");
        self.img.src = self.sourceURLs[0];
        self.img.style["max-height"] = window.innerHeight + "px";
        imageContainer.appendChild(self.img);
        resolve(imageContainer);
    });
};

Redditmod.VideoPromise = function(sourceURLs) {
    return new Promise(function(resolve, reject) {
        var video = document.createElement("video");
        video.controls = false;
        video.autoplay = true;
        video.loop = true;
        video.classList.add("eddit-content-video");
        video.style.display = "block";
        video.style.width = "auto";
        video.style.height = "auto";
        sourceURLs.forEach(function(sourceURL) {
            var source = document.createElement("source");
            source.src = sourceURL;
            video.appendChild(source);
        });
        resolve(video);
    });
};

Redditmod.GiphyPromise = function(url) {
    // https://giphy.com/gifs/xUPGctxgaSqOpZx9zW
    // https://media.giphy.com/media/xUPGctxgaSqOpZx9zW/giphy.gif
    // https://media.giphy.com/media/xUPGctxgaSqOpZx9zW/giphy.mp4
    var matches = url.href.match(/giphy\.com\/(?:gifs|media)\/(?:[a-z0-9\-]*-)?([a-z0-9]+)/i);
    if (!matches) return null;
    var shortcode = matches[1];
    return Redditmod.VideoPromise([
        "https://media.giphy.com/media/" + shortcode + "/giphy.mp4"
    ]);
};

Redditmod.GfycatPromise = function(url) {
    var strippedUrl = url.href.replace(/(www\.|giant\.|thumbs\.|zippy\.|fat\.|\.webm|\.mp4|\.gif$|#.*$|\?.*$)/g, "");
    return Redditmod.VideoPromise([
        strippedUrl.replace("gfycat",   "fat.gfycat") + ".webm",
        strippedUrl.replace("gfycat", "zippy.gfycat") + ".webm",
        strippedUrl.replace("gfycat", "giant.gfycat") + ".webm"
    ]);
};

Redditmod.StreamablePromise = function(url) {
    var matches = url.href.match(/streamable\.com\/([a-zA-Z0-9]*)/);
    if (!matches) return;
    var shortcode = matches[1];
    var apiUrl = "https://api.streamable.com/videos/" + shortcode;
    return new Promise(function(resolve, reject) {
        GM_xmlhttpRequest({
            method: "GET",
            url: apiUrl,
            onload: function(response) {
                try {
                    var json = JSON.parse(response.responseText);
                    Redditmod.VideoPromise([json.files.mp4.url]).then(resolve, reject);
                } catch (error) {
                    reject("Error (" + error + "): Failed to read " + apiUrl);
                }
            }
        });
    });
};

Redditmod.XkcdPromise = function(url) {
    var matches = url.href.match(/xkcd\.com\/([0-9]+)/);
    if (!matches) return;
    var shortcode = matches[1];
    var apiUrl = "https://xkcd.com/" + shortcode + "/info.0.json";
    return new Promise(function(resolve, reject) {
        GM_xmlhttpRequest({
            method: "GET",
            url: apiUrl,
            onload: function(response) {
                try {
                    var json = JSON.parse(response.responseText);
                    var xkcdDiv = document.createElement("div");
                    var h3 = document.createElement("h3");
                    h3.textContent = json.title;
                    var img = document.createElement("img");
                    img.src = json.img;
                    img.title = json.alt;
                    var h5 = document.createElement("h5");
                    h5.textContent = json.alt;
                    xkcdDiv.appendChild(h3);
                    xkcdDiv.appendChild(img);
                    xkcdDiv.appendChild(h5);
                    resolve(xkcdDiv);
                } catch (error) {
                    reject("Error (" + error + "): Failed to read " + apiUrl);
                }
            }
        });
    });
};

Redditmod.InstagramPromise = function(url) {
    var theUrl = url.href;
    var matches = theUrl.match(/instagram\.com\/p\/([a-zA-Z0-9_\-]*)/);
    if (!matches) reject("No images found", theUrl);
    var shortcode = matches[1];
    var apiUrl = "https://instagram.com/p/" + shortcode + "/";
    return new Promise(function(resolve, reject) {
        GM_xmlhttpRequest({
            method: "GET",
            url: apiUrl,
            onload: function(response) {
                try {
                    var html = document.createElement("html");
                    html.innerHTML = response.responseText;
                    var videoMeta = html.querySelector('meta[property="og:video"]');
                    var imageMeta = html.querySelector('meta[property="og:image"]');
                    if (videoMeta) {
                        Redditmod.VideoPromise([videoMeta.getAttribute("content")]).then(resolve, reject);
                    } else if (imageMeta) {
                        Redditmod.ImagePromise([imageMeta.getAttribute("content")]).then(resolve, reject);
                    } else {
                        reject("No images found ", apiUrl);
                    }
                } catch (error) {
                    reject("Error (" + error + "): Failed to read " + apiUrl);
                }
            }
        });
    });
};

Redditmod.ImgurPromise = function(url) {
    var href = url.href.replace(/\?.*/, "");
    if (href.indexOf("/a/") >= 0 || href.indexOf("/gallery/") >= 0) {
        return Redditmod.ImgurAlbumPromise(href);
    } else if (/\.gifv$/.test(href) || /\.gif$/.test(href) || /\.mp4$/.test(href)) {
        // it's a GIF/video.
        href = href.replace(/\.(gifv|gif|mp4)$/, ".mp4");
        return Redditmod.VideoPromise([href]);
    } else {
        href = href.replace(/[^/]*\.imgur\.com/, "i.imgur.com");
        href = href.replace(/_[a-z]./, ".");
        href = href.replace(/\.(gif|jpg|jpeg|png)$/i, "");
        href = href + ".jpg";
        return Redditmod.ImagePromise([href]);
    }
};

Redditmod.ImgurAlbumPromise = function(url) {
    var theUrlForReal = url;
    return new Promise(function(resolve, reject) {
        GM_xmlhttpRequest({
            method: "GET",
            url: theUrlForReal,
            onload: function(response) {
                // Parsing imgur album HTML for Javascript via Regex. Lord'avmercy
                try {
                    var jsonChunks = response.response.match(/\s*image\s*:\s*(.*),\s*/);
                    var json = JSON.parse(jsonChunks[1] || "{}");
                    var album_images = json.album_images || {};
                    var images = album_images.images || [];
                    if (images.length === 0) {
                        // No images, it might be a "gallery" link.
                        if (/imgur\.com\/gallery/.test(theUrlForReal)) {
                            var imgurHtml = document.createElement("html");
                            imgurHtml.innerHTML = response.responseText;
                            var imgurImage = imgurHtml.querySelector('link[rel="image_src"]');
                            if (imgurImage) {
                                Redditmod.ImagePromise([imgurImage.getAttribute("href")]).then(resolve, reject);
                            } else {
                                reject("No images found ", theUrlForReal);
                            }
                        } else {
                            reject("No images found ", theUrlForReal);
                        }
                    } else {
                        var urls = images.map(function(image) {
                            return "https://i.imgur.com/" + image.hash + image.ext;
                        });
                        Redditmod.ImagePromise(urls).then(resolve, reject);
                    }
                } catch (error) {
                    reject("Error (" + error + "): Failed to load imgur album ");
                }
            }
        });
    });
};

Redditmod.FlickrPromise = function(url) {
    var theUrlForReal = url.href;
    return new Promise(function(resolve, reject) {
        GM_xmlhttpRequest({
            method: "GET",
            url: theUrlForReal,
            onabort: reject,
            onerror: reject,
            onload: function(response) {
                // Parsing flickr HTML for Javascript via Regex. Lord'avmercy
                try {
                    var jsonChunks = response.response.match(/modelExport: (\{.*})/);
                    var json = JSON.parse(jsonChunks[1] || "{}");
                    var photo_models = json["photo-models"] || [];
                    var images = photo_models.map(function(model) {
                        var imageObjs = [];
                        for (var key in model.sizes) {
                            imageObjs.push(model.sizes[key]);
                        }
                        imageObjs = imageObjs.sort(function(a,b) {
                            return a.width < b.width;
                        });
                        if (imageObjs.length > 0) {
                            return window.location.protocol + imageObjs[0].url;
                        } else {
                            return null;
                        }
                    }).filter(function(imageUrl) {
                        return imageUrl !== null;
                    });
                    if (images.length === 0) {
                        reject("No images found ", theUrlForReal);
                    } else {
                        Redditmod.ImagePromise(images).then(resolve, reject);
                    }
                } catch (error) {
                    reject("Error (" + error + "): Failed to load Flickr page ");
                }
            }
        });
    });
};

Redditmod.RedditCommentsPromise = function(url) {
    var theUrlForReal = url.href;
    return new Promise(function(resolve, reject) {
        GM_xmlhttpRequest({
            method: "GET",
            url: theUrlForReal,
            onabort: reject,
            onerror: reject,
            onload: function(response) {
                try {
                    var html = document.createElement("html");
                    html.innerHTML = response.responseText;
                    var commentContainer = html.querySelector(".commentarea > .sitetable");
                    if (commentContainer) {
                        // Process incoming comments
                        commentContainer.querySelectorAll(".thing.comment").forEach(Redditmod.Comments.add);
                        resolve(commentContainer);
                    } else {
                        reject("Failed to find commentarea at ", theUrlForReal);
                    }
                } catch (error) {
                    reject("Error (" + error + "): Failed to load page " + theUrlForReal);
                }
            }
        });
    });
};

Redditmod.OtherPromise = function(url) {
    var theUrl = url.href;
    return new Promise(function(resolve, reject) {
        GM_xmlhttpRequest({
            method: "GET",
            headers: {"X-api-key": "NtFdFjTYzQXF4WUWBivfsnTj0zXZyvwCKbSQeuAB"},
            url: "https://mercury.postlight.com/parser?url=" + encodeURIComponent(theUrl),
            onload: function(response) {
                try {
                    var json = JSON.parse(response.response);
                    var otherContent = document.createElement("div");
                    otherContent.innerHTML = json.content;
                    otherContent.classList.add("eddit-content-other");
                    resolve(otherContent);
                } catch (error) {
                    reject("Error (" + error + "): Failed to load page ");
                }
            },
            onerror: function(xhr) {
                reject("Error (status:" + xhr.status + " " + xhr.statusText + ") ");
            },
            onabort: function(xhr) {
                reject("Error (status:" + xhr.status + " " + xhr.statusText + ") ");
            }
        });
    });
};

Redditmod.VisitedLinks = (function() {
    var self = this;
    this._visitedLinks = GM_getValue("eddit-visited-links", {});
    this.contains = function(link) {
        return (self._visitedLinks[link] === true);
    };
    this.add = function(link) {
        if (!self._visitedLinks[link]) {
            self._visitedLinks[link] = true;
            GM_setValue("eddit-visited-links", self._visitedLinks);
        }
    };
    return {
        contains: this.contains,
        add: this.add
    };
})();

Redditmod.MediaHandler = function(domPost) {
    if (!(this instanceof Redditmod.MediaHandler)) return new Redditmod.MediaHandler(domPost);
    var self = this;

    this._domPost = domPost;
    this._loaded = false;
    this._expanded = false;
    this._mediaObj = null;
    this._commentsObj = null;

    this._shouldUseExpando = self._domPost.classList.contains("self");

    this.url = (function() {
        var thisUrl = self._domPost.getAttribute("data-url");
        if (thisUrl.indexOf("/") === 0) {
            thisUrl = window.location.protocol + "//" + window.location.host + thisUrl;
        }
        return new URL(thisUrl);
    })();

    this._load = function() {
        if (self._loaded) return;
        self._loaded = true;
        self._expanded = true;
        if (self._domPost.classList.contains("self")) {
            self._shouldUseExpando = true;
            return;
        }
        var mediaPromise = Redditmod.MediaPromise(self.url);
        if (mediaPromise instanceof Promise) {
            mediaPromise.then(function(mediaDiv) {
                mediaDiv.style["max-width"] = self._domPost.clientWidth + "px";
                self._mediaObj = mediaDiv;
                self._domPost.appendChild(mediaDiv);
            }).catch(Redditmod.Error);
        } else {
            self._shouldUseExpando = true;
        }
    };

    this._showMedia = function() {
        self._load();
        self._expanded = true;
        if (self._shouldUseExpando) {
            self._clickExpando();
        } else if (self._mediaObj) {
            self._mediaObj.style.display = "block";
        }
        if (self._expandoButton && self._expandoButton.classList.contains("collapsed")) {
            self._expandoButton.classList.add("expanded");
            self._expandoButton.classList.remove("collapsed");
        }
    };
    this._hideMedia = function() {
        self._load();
        self._expanded = false;
        if (self._shouldUseExpando) {
            self._clickExpando();
        } else if (self._mediaObj) {
            self._mediaObj.style.display = "none";
        }
        if (self._expandoButton && self._expandoButton.classList.contains("expanded")) {
            self._expandoButton.classList.add("collapsed");
            self._expandoButton.classList.remove("expanded");
        }
    };

    this.markVisited = function() {
        var linkTitle = self._domPost.querySelector("a.title");
        if (!linkTitle) return;
        linkTitle.style.color = Redditmod.CSS.colors.fg.visited;
    };

    this._clickExpando = function() {
        if (self._expandoButton) {
            self._expandoButton.click();
        } else if (self._expandoButton.classList.contains("expanded")) {
            self._expandoButton.classList.remove("expanded");
        } else {
            self._expandoButton.classList.add("expanded");
        }
    };

    this._click = function(event) {
        var target = Redditmod.Utils.findThing(event);
        if (!target) return;
        target.scrollIntoView({behavior: "smooth"});
        self._toggle(event);
    };

    this._toggle = function(e) {
        e.stopPropagation();
        e.preventDefault();
        Redditmod.VisitedLinks.add(self.url.href);
        self.markVisited();

        if (self._expanded) {
            self._hideMedia();
        } else {
            self._showMedia();
        }
    };

    this._expandoButton = (function() {
        var button = self._domPost.querySelector(".expando-button");
        if (!button) {
            button = document.createElement("a");
            button.classList.add("expando-button");
            button.classList.add("collapsed");
            button.classList.add("video");
            button.onclick = self._toggle;
            var entry = self._domPost.querySelector(".entry");
            var tagline = self._domPost.querySelector(".tagline");
            entry.insertBefore(button, tagline);
        }
        return button;
    })();

    self._domPost.addEventListener("click", self._click);
};

// top-level domain name (no subdomains)
var DOMAIN_NAME_REGEX = RegExp(/([a-z0-9\-]+\.[a-z]{2,}$)/);

/**
 * @returns Promise for a <div> holding the content found at URL.
 *          Returns null if reddit's built-in expando should be used.
 */
Redditmod.MediaPromise = function(url) {
    if (!(this instanceof Redditmod.MediaPromise)) return new Redditmod.MediaPromise(url);
    var host, hostMatches = DOMAIN_NAME_REGEX.exec(url.host);
    host = hostMatches ? hostMatches[1] : url.host;

    if (host === "youtube.com" || host === "youtu.be" || host === "vimeo.com") {
        return null; // Should use expando
    }

    // Custom media Promises
    var hostToPromise = {
        "gfycat.com": Redditmod.GfycatPromise,
        "imgur.com": Redditmod.ImgurPromise,
        "xkcd.com": Redditmod.XkcdPromise,
        "instagram.com": Redditmod.InstagramPromise,
        "flickr.com": Redditmod.FlickrPromise,
        "streamable.com": Redditmod.StreamablePromise,
        "reddit.com": Redditmod.RedditCommentsPromise,
        "giphy.com": Redditmod.GiphyPromise
    };
    if (host in hostToPromise) {
        return hostToPromise[host](url);
    }

    var isImage = (/\.(gif|jpg|jpeg|png)/i.test(url.href) || host === "reddituploads.com");
    if (isImage) {
        return Redditmod.ImagePromise([url.href]);
    }

    return Redditmod.OtherPromise(url);
};

Redditmod.SubredditFilter = function(subreddit, enabled) {
    if (!(this instanceof Redditmod.SubredditFilter)) return new Redditmod.SubredditFilter(subreddit, enabled);
    var self = this;
    self.subreddit = subreddit;
    self.enabled = enabled;
    self.filterLink = null;

    this.init = function() {
        self.filterLink = document.createElement("a");
        self.filterLink.href = "#";
        self.filterLink.classList.add("choice");
        self.filterLink.textContent = self.subreddit;
        self.filterLink.addEventListener("click", function(e) {
            e.stopPropagation();
            e.preventDefault();
            self.toggle();
            Redditmod.SubredditFilters.save();
        });

        if (self.enabled) {
            self.enable();
        } else {
            self.disable();
        }
        var dropdown = document.querySelector("#sr-header-area .drop-choices");
        dropdown.appendChild(self.filterLink);
    };

    this.disable = function() {
        self.filterLink.classList.add("eddit-subreddit-disabled");
        self.filterLink.classList.remove("eddit-subreddit-enabled");
        self.filterLink.innerHTML = "&#9744; " + self.subreddit;
        self.enabled = false;
    };
    this.enable = function() {
        self.filterLink.classList.add("eddit-subreddit-enabled");
        self.filterLink.classList.remove("eddit-subreddit-disabled");
        self.filterLink.innerHTML = "&#9745; " + self.subreddit;
        self.enabled = true;
    };
    this.toggle = function() {
        if (self.enabled) {
            self.disable();
        } else {
            self.enable();
        }
    };

    this.init();
};

Redditmod.NsfwFilter = (function() {
    var self = this;
    this.enabled = GM_getValue("eddit-nsfw-filter", false);
    this.filter = document.createElement("a");
    this.filter.href = "#";
    this.filter.classList.add("choice");
    this.filter.addEventListener("click", function(e) {
        e.stopPropagation();
        e.preventDefault();
        self.enabled = !self.enabled;
        GM_setValue("eddit-nsfw-filter", self.enabled);
        self.refreshNsfwFilter();
    });
    this.refreshNsfwFilter = function() {
        if (self.enabled) {
            self.filter.innerHTML = "&#9745; NSFW Filter";
        } else {
            self.filter.innerHTML = "&#9744; NSFW Filter";
        }
        if (Redditmod.Posts) {
            Redditmod.Posts.refresh();
        }
    };
    var dropdown = document.querySelector("#sr-header-area .drop-choices");
    dropdown.appendChild(document.createElement("hr"));
    dropdown.appendChild(this.filter);
    this.refreshNsfwFilter();
    return this;
})();

/**
 * Wrapper around filtered-subreddits config.
 * Usage:
 *   if (!Redditmod.SubredditFilters.isFiltered("wtf")) {
 *       Redditmod.SubredditFilters.add("wtf");
 *   }
 */
Redditmod.SubredditFilters = (function() {
    var self = this;

    this._filters = {};
    this._load = function() {
        var dropdown = document.querySelector("#sr-header-area .drop-choices");
        dropdown.appendChild(document.createElement("hr"));

        var filterHeader = document.createElement("h4");
        filterHeader.textContent = "Filtered Subreddits";
        dropdown.appendChild(filterHeader);

        var selectAll = document.createElement("a");
        selectAll.textContent = "Filter All";
        selectAll.href = "#";
        selectAll.style["padding-left"] = "10px";
        selectAll.style["font-size"] = "0.8em";
        selectAll.addEventListener("click", function(e) {
            e.stopPropagation();
            e.preventDefault();
            Object.keys(self._filters).forEach(function(key) {
                self._filters[key].enable();
            });
            Redditmod.Posts.refresh();
        });
        dropdown.appendChild(selectAll);

        var selectNone = document.createElement("a");
        selectNone.textContent = "Filter None";
        selectNone.href = "#";
        selectNone.style["padding-left"] = "10px";
        selectNone.style["font-size"] = "0.8em";
        selectNone.addEventListener("click", function(e) {
            e.stopPropagation();
            e.preventDefault();
            Object.keys(self._filters).forEach(function(key) {
                self._filters[key].disable();
            });
            Redditmod.Posts.refresh();
        });
        dropdown.appendChild(selectNone);

        var subData = GM_getValue("eddit-filtered-subreddits", {});
        for (var subreddit in subData) {
            self._filters[subreddit] = Redditmod.SubredditFilter(subreddit, subData[subreddit]);
        }
        if (Redditmod.Posts) {
            Redditmod.Posts.refresh();
        }
    };
    this._stripAndLower = function(sub) {
        sub = sub || "";
        return sub.replace(/(^ +| +$)/, "").toLowerCase();
    };

    this._shouldFilterPage = function() {
        var path = window.location.pathname;
        if (/\/r\//.test(path)) {
            var sub = path.match(/\/r\/([^?#\/]*)/)[1];
            return sub === "all" || sub === "popular";
        } else {
            return false;
        }
    };

    this.isFiltered = function(sub) {
        var strippedSub = self._stripAndLower(sub);
        if (self._shouldFilterPage() && strippedSub in self._filters) {
            return self._filters[strippedSub].enabled === true;
        } else {
            return false;
        }
    };
    this.add = function(sub) {
        var strippedSub = self._stripAndLower(sub);
        if (!(strippedSub in self._filters)) {
            self._filters[strippedSub] = new Redditmod.SubredditFilter(strippedSub, true);
        }
        self._filters[strippedSub].enable();
        self.save();
    };
    this.save = function() {
        var toSave = {};
        for (var sub in self._filters) {
            toSave[sub] = self._filters[sub].enabled;
        }
        GM_setValue("eddit-filtered-subreddits", toSave);
        if (Redditmod.Posts) {
            Redditmod.Posts.refresh();
        }
    };

    this._load();
    return {
        add: this.add,
        save: this.save,
        isFiltered: this.isFiltered
    };
})();

/**
 * Represents a post ("thing" in reddit-terms).
 * @param thingElement - Thing DOM element on the page.
 * Usage:
 *   var thing = Redditmod.Post(document.querySelector(".thing"));
 */
Redditmod.Post = function(domPost) {
    if (!(this instanceof Redditmod.Post)) return new Redditmod.Post(domPost);
    var self = this;
    this.element = domPost;
    this.subreddit = this.element.getAttribute("data-subreddit");
    this.mediaHandler = new Redditmod.MediaHandler(this.element);

    this.init = function() {
        self._addFilterLink();
        self.refresh();
        if (document.querySelectorAll('#siteTable .thing.link[id="' + self.element.id + '"]').length > 1) {
            self.element.classList.add("eddit-duplicate");
        }
    };

    this._addFilterLink = function() {
        var filterLink = document.createElement("a");
        filterLink.innerHTML = "&times;";
        filterLink.href = "#";
        filterLink.title = "Filter /r/" + self.subreddit + " from appearing";
        filterLink.classList.add("eddit-filter-subreddit-link");
        filterLink.addEventListener("click", self._filterLinkClick);

        var tagLine = self.element.querySelector(".tagline");
        if (tagLine) {
            tagLine.appendChild(filterLink);
        }
    };

    this._filterLinkClick = function(e) {
        e.stopPropagation();
        e.preventDefault();
        Redditmod.SubredditFilters.add(self.subreddit);
    };

    this.hide = function() { self.element.classList.add("eddit-filtered-post"); };
    this.show = function() { self.element.classList.remove("eddit-filtered-post"); };
    this.refresh = function() {
        if (Redditmod.SubredditFilters.isFiltered(self.subreddit)) {
            self.hide();
        } else if (Redditmod.NsfwFilter.enabled && self.element.classList.contains("over18")) {
            self.hide();
        } else {
            if (Redditmod.VisitedLinks.contains(self.mediaHandler.url)) {
                self.mediaHandler.markVisited();
            }
            self.show();
        }
    };

    this.clickExpando = function() {
        var button = self.element.querySelector(".expando-button");
        if (button) {
            button.click();
            return true;
        } else {
            return false;
        }
    };

    this.markVisited = function() {
        var linkTitle = self.element.querySelector("a.title");
        if (linkTitle) {
            var style = Redditmod.CSS.colors.fg.visited;
            linkTitle.style.color = style;
        }
    };

    this.init();
};

Redditmod.Posts = (function() {
    var self = this;
    this._things = [];

    this.init = function() {
        var postContainer = document.querySelector("#siteTable");
        if (!postContainer) return;
        var domPosts = postContainer.querySelectorAll(".thing.link");
        domPosts.forEach(function(domPost) {
            self._add(domPost);
        });
        self._postListener.observe(postContainer, {childList: true});
    };

    this._postListener = new MutationObserver(function(mutations) {
        mutations.forEach(function(mutation) {
            mutation.addedNodes.forEach(function(addedNode) {
                if (addedNode.classList.contains("thing")) {
                    self._add(addedNode);
                }
            });
        });
    });

    this._add = function(thingElement) {
        self._things.push(new Redditmod.Post(thingElement));
    };

    this.refresh = function() {
        self._things.forEach(function(thing) {
            thing.refresh();
        });
    };

    this.init();

    return this;
})();

Redditmod.Nav = (function() {
    var self = this;

    // Flag when we are already loading the next page.
    this.loading = false;

    this.init = function() {
        self.addScrollListener();
        self._scrollListener();
        self.overrideNextButton();
    };

    // Load more posts when user scrolls near bottom of the page.
    this.addScrollListener = function() {
        window.addEventListener("scroll", self._scrollListener);
    };
    this.removeScrollListener = function() {
        window.removeEventListener("scroll", self._scrollListener);
    };
    this._scrollListener = function(event) {
        var evt = event || {pageY:0};
        if (document.body.clientHeight - (window.scrollY + window.innerHeight) < 200) {
            self.loadMorePosts();
        }
    };

    // Instead of navigating to the next page, use AJAX to load the posts.
    this.overrideNextButton = function() {
        var nextButton = document.querySelector(".next-button a");
        if (!nextButton) return;
        nextButton.addEventListener("click", function(e) {
            e.stopPropagation();
            e.preventDefault();
            self.loadMorePosts();
        });
    };

    // Inserts reddit posts from the AJAX response onto the current page.
    this._injectPosts = function(response) {
        var nav = document.querySelector(".nav-buttons");
        // Convert AJAX response to DOM, add just the posts to the current page.
        var nextPage = document.createElement("html");
        nextPage.innerHTML = response.responseText;
        nextPage.querySelectorAll("#siteTable > *").forEach(function(otherElement) {
            nav.parentNode.insertBefore(otherElement, nav); 
        });
        nav.parentNode.removeChild(nav);

        // Re-enable features on the "new page".
        self.overrideNextButton();
        self.addScrollListener();
        self.loading = false;
        setTimeout(self._scrollListener, 250);
    };

    // Fetches posts from the current page's "next" button.
    this.loadMorePosts = function() {
        var nextButton = document.querySelector(".next-button a");
        if (!self.loading && nextButton) {
            self.loading = true;
            self.removeScrollListener();
            GM_xmlhttpRequest({
                method: "GET",
                url: nextButton.href,
                onload: self._injectPosts
            });

            var parentNode = nextButton.parentNode.parentNode;
            parentNode.style["background-color"] = "#aaa";
            parentNode.opacity = "0.5";
            parentNode.cursor = "not-allowed";
            parentNode.childNodes.forEach(function(child) {
                if (child.style) {
                    child.style.display = "none";
                }
            });
        }
    };

    this.init();
})();

Redditmod.Utils = (function() {
    var self = this;

    /** Looks at the parents of the event's target until it hits a ".thing" */
    this.findThing = function(event) {
        var IGNORED_CLASSES = ["expando-button", "midcol"];
        var UNIGNORED_CLASSES = ["thumbnail"];
        var IGNORED_TAGS = ["A", "INPUT", "TEXTAREA", "BUTTON"];
        var target = event.target, ignoredClass, ignoredTag, doNotIgnore, shouldIgnore;
        while (!target.classList.contains("thing")) {
            ignoredClass = IGNORED_CLASSES.find(function(c) { return target.classList.contains(c); }) !== undefined;
            ignoredTag = IGNORED_TAGS.indexOf(target.tagName.toUpperCase()) >= 0;
            doNotIgnore = UNIGNORED_CLASSES.find(function(c) { return target.classList.contains(c); }) !== undefined;
            shouldIgnore = (ignoredClass || ignoredTag) && !doNotIgnore;
            if (shouldIgnore) return null;

            target = target.parentElement;
            if (!target) return null;
        }
        return target;
    };

    return this;
})();

Redditmod.Comment = function(domComment) {
    if (!(this instanceof Redditmod.Comment)) return new Redditmod.Comment(domComment);
    var self = this;
    this.element = domComment;
    this.toggleCollapse = function(e) {
        var target = Redditmod.Utils.findThing(e);
        if (!target) return;
        e.stopPropagation();
        e.preventDefault();
        if (target.classList.contains("noncollapsed")) {
            target.classList.remove("noncollapsed");
            target.classList.add("collapsed");
        } else {
            target.classList.remove("collapsed");
            target.classList.add("noncollapsed");
        }
    };
    domComment.querySelector(".entry").addEventListener("mouseenter", function(e) {
        document.querySelectorAll(".eddit-comment-hover").forEach(function(element) {
            element.classList.remove("eddit-comment-hover");
        });
        self.element.classList.add("eddit-comment-hover");
    });
    domComment.querySelector(".entry").addEventListener("mouseleave", function(e) {
        self.element.classList.remove("eddit-comment-hover");
    });
    domComment.addEventListener("click", this.toggleCollapse);
};

Redditmod.Comments = (function() {
    var self = this;
    this._comments = [];

    this.add = function(domComment) {
        self._comments.push(Redditmod.Comment(domComment));
    };

    document.querySelectorAll(".thing.comment").forEach(self.add);
    return {
        add: this.add
    };
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址