redditmod

Subset of RES features I like.

13.05.2017 itibariyledir. En son verisyonu görün.

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

You will need to install an extension such as Tampermonkey to install this script.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği indirebilmeniz için ayrıca Tampermonkey gibi bir eklenti kurmanız gerekmektedir.

Bu komut dosyasını yüklemek için bir kullanıcı komut dosyası yöneticisi uzantısı yüklemeniz gerekecek.

(Zaten bir kullanıcı komut dosyası yöneticim var, kurmama izin verin!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Zateb bir user-style yöneticim var, yükleyeyim!)

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