// ==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 = "☐ " + 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 = "☑ " + 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 = "☑ NSFW Filter";
} else {
self.filter.innerHTML = "☐ 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 = "×";
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
};
})();