redditmod

Subset of RES features I like.

目前為 2017-07-12 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

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

/*
TODO Features:
-> 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.
*/

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", ".sidebox .spacer"
        ],
        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", ".recommended-link"
        ],
        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 gfycatUrl, shortCode = url.href.match(/gfycat\.com\/.*\/([a-z0-9]*)/i);
    if (!shortCode) {
        gfycatUrl = url.href;
    } else {
        gfycatUrl = "https://gfycat.com/" + shortCode[1];
    }
    return new Promise(function(resolve, reject) {
        GM_xmlhttpRequest({
            method: "GET",
            url: gfycatUrl,
            onabort: reject,
            onerror: reject,
            onload: function(response) {
                try {
                    var html = document.createElement("html");
                    html.innerHTML = response.responseText;
                    var videoSource = html.querySelector("#webmSource").src;
                    Redditmod.VideoPromise([videoSource]).then(resolve, reject);
                } catch (error) {
                    reject("Error (" + error + "): Failed to read " + gfycatUrl);
                }
            }
        });
    });
};

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,
            onabort: reject,
            onerror: reject,
            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.TwitchClipPromise = function(url) {
    var twitchUrl = url.href;
    return new Promise(function(resolve, reject) {
        GM_xmlhttpRequest({
            method: "GET",
            url: twitchUrl,
            onabort: reject,
            onerror: reject,
            onload: function(response) {
                try {
                    var matches = response.responseText.match(/quality_options: (\[.*\]),/);
                    if (!matches) {
                        reject("Error: No 'quality_options' at " + twitchUrl);
                    } else {
                        var json = JSON.parse(matches[1]);
                        Redditmod.VideoPromise([json[0].source]).then(resolve, reject);
                    }
                } catch (error) {
                    reject("Error (" + error + "): Failed to read " + twitchUrl);
                }
            }
        });
    });
};

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,
            onabort: reject,
            onerror: reject,
            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.DeviantartPromise = function(url) {
    var theUrl = url.href;
    return new Promise(function(resolve, reject) {
        GM_xmlhttpRequest({
            method: "GET",
            url: theUrl,
            onabort: reject,
            onerror: reject,
            onload: function(response) {
                try {
                    var html = document.createElement("html");
                    html.innerHTML = response.responseText;
                    var fullImg = html.querySelector('img[dev-content-full]');
                    var smallImg = html.querySelector('meta[property="og:image"]');
                    if (fullImg) {
                        Redditmod.ImagePromise([fullImg.getAttribute("src")]).then(resolve, reject);
                    } else if (smallImg) {
                        Redditmod.ImagePromise([smallImg.getAttribute("content")]).then(resolve, reject);
                    } else {
                        reject("No images found ", theUrl);
                    }
                } catch (error) {
                    reject("Error (" + error + "): Failed to read ", theUrl);
                }
            }
        });
    });
};

Redditmod.TwitterPromise = function(url) {
    var theUrl = url.href;
    return new Promise(function(resolve, reject) {
        GM_xmlhttpRequest({
            method: "GET",
            url: theUrl,
            onabort: reject,
            onerror: reject,
            onload: function(response) {
                try {
                    var html = document.createElement("html");
                    html.innerHTML = response.responseText;
                    var tweetContainer = html.querySelector('.permalink');
                    if (tweetContainer) {
                        tweetContainer.querySelectorAll(".follow-bar").forEach(function(e) { e.parentNode.removeChild(e); });
                        tweetContainer.querySelectorAll(".ProfileTweet-action").forEach(function(e) { e.parentNode.removeChild(e); });
                        tweetContainer.querySelectorAll(".avatar-row").forEach(function(e) { e.parentNode.removeChild(e); });
                        tweetContainer.querySelectorAll(".u-hiddenVisually").forEach(function(e) { e.parentNode.removeChild(e); });
                        tweetContainer.querySelectorAll(".stream-footer").forEach(function(e) { e.parentNode.removeChild(e); });
                        tweetContainer.querySelectorAll(".stream-fail-container").forEach(function(e) { e.parentNode.removeChild(e); });
                        tweetContainer.querySelectorAll(".hidden-replies-container").forEach(function(e) { e.parentNode.removeChild(e); });
                        tweetContainer.querySelectorAll(".FullNameGroup").forEach(function(e) { e.parentNode.removeChild(e); });
                        tweetContainer.querySelector(".replies-to").style.display = "block";
                        resolve(tweetContainer);
                    } else {
                        reject("No .permalink tweet found ", theUrl);
                    }
                } catch (error) {
                    reject("Error (" + error + "): Failed to read ", theUrl);
                }
            }
        });
    });
};

Redditmod.LightshotPromise = function(url) {
    var lightshotUrl = url.href;
    return new Promise(function(resolve, reject) {
        GM_xmlhttpRequest({
            method: "GET",
            url: lightshotUrl,
            onabort: reject,
            onerror: reject,
            onload: function(response) {
                try {
                    var html = document.createElement("html");
                    html.innerHTML = response.responseText;
                    var imageMeta = html.querySelector('meta[property="og:image"]');
                    if (imageMeta) {
                        Redditmod.ImagePromise([imageMeta.getAttribute("content")]).then(resolve, reject);
                    } else {
                        reject("No images found ", lightshotUrl);
                    }
                } catch (error) {
                    reject("Error (" + error + "): Failed to read " + lightshotUrl);
                }
            }
        });
    });
};

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,
            onabort: reject,
            onerror: reject,
            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,
            onabort: reject,
            onerror: reject,
            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._domCommentsLink = domPost.querySelector(".buttons a.comments");
    this._loadedMedia = false;
    this._loadedComments = false;
    this._expandedMedia = false;
    this._expandedComments = 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) {
            return null;
        }
        if (thisUrl && thisUrl.indexOf("/") === 0) {
            thisUrl = window.location.protocol + "//" + window.location.host + thisUrl;
        }
        try {
            return new URL(thisUrl);
        } catch (e) {
            return thisUrl;
        }
    })();

    this._loadMedia = function() {
        if (self._loadedMedia) return;
        self._loadedMedia = true;
        self._expandedMedia = true;
        if (self._expandedComments) self._hideComments();
        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._loadMedia();
        self._expandedMedia = true;
        if (self._shouldUseExpando) {
            self._clickExpando();
        } else if (self._mediaObj) {
            self._mediaObj.style.display = "block";
            if (self._mediaObj.tagName === "VIDEO") self._mediaObj.load();
        }
        if (self._expandoButton && self._expandoButton.classList.contains("collapsed")) {
            self._expandoButton.classList.add("expanded");
            self._expandoButton.classList.remove("collapsed");
        }
    };

    this._hideMedia = function() {
        if (!self._loadedMedia) return;
        self._loadMedia();
        self._expandedMedia = false;
        if (self._shouldUseExpando) {
            self._clickExpando();
        } else if (self._mediaObj) {
            self._mediaObj.style.display = "none";
            if (self._mediaObj.tagName === "VIDEO") self._mediaObj.pause();
        }
        if (self._expandoButton && self._expandoButton.classList.contains("expanded")) {
            self._expandoButton.classList.add("collapsed");
            self._expandoButton.classList.remove("expanded");
        }
    };

    this._loadComments = function() {
        if (self._loadedComments) return;
        if (!self._domCommentsLink) return;
        self._loadedComments = true;
        self._expandedComments = true;
        var commentsUrl = new URL("https://reddit.com" + self._domCommentsLink.getAttribute("href"));
        var commentsPromise = Redditmod.RedditCommentsPromise(commentsUrl);
        commentsPromise.then(function(commentsDiv) {
            commentsDiv.style["max-width"] = self._domPost.clientWidth + "px";
            self._commentsObj = commentsDiv;
            self._domPost.appendChild(commentsDiv);
        }).catch(function(reason) {
            self._domPost.appendChild(Redditmod.Error(reason, commentsUrl));
        });
    };

    this._showComments = function() {
        self._loadComments();
        self._expandedComments = true;
        if (self._commentsObj) {
            self._commentsObj.style.display = "block";
        }
    };

    this._hideComments = function() {
        if (!self._loadedComments) return;
        self._loadComments();
        self._expandedComments = false;
        if (self._commentsObj) {
            self._commentsObj.style.display = "none";
        }
    };

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

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

    this._toggleComments = function(e) {
        e.stopPropagation();
        e.preventDefault();
        if (self._expandedMedia) self._hideMedia();

        if (self._expandedComments) {
            self._hideComments();
        } else {
            self._showComments();
        }
    };

    this._toggleMedia = function(e) {
        e.stopPropagation();
        e.preventDefault();
        if (self._expandedComments) self._hideComments();
        if (!self.url) return;
        Redditmod.VisitedLinks.add(self.url.href);
        self.markVisited();

        if (self._expandedMedia) {
            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._toggleMedia;
            var entry = self._domPost.querySelector(".entry");
            var tagline = self._domPost.querySelector(".tagline");
            // TODO: Apparently this doesn't work because button isn't a child of entry, or tagline, or something.
            //entry.insertBefore(button, tagline);
        }
        return button;
    })();

    if (self._domPost) {
        self._domPost.addEventListener("click", self._postClick);
    }
    if (self._domCommentsLink) {
        self._domCommentsLink.addEventListener("click", self._toggleComments);
    }
};

// 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,
        "twitch.tv": Redditmod.TwitchClipPromise,
        "reddit.com": Redditmod.RedditCommentsPromise,
        "giphy.com": Redditmod.GiphyPromise,
        "deviantart.com": Redditmod.DeviantartPromise,
        "twitter.com": Redditmod.TwitterPromise,
        "prnt.sc": Redditmod.LightshotPromise
    };
    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
    };
})();