您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Subset of RES features I like.
当前为
// ==UserScript== // @name redditmod // @namespace derv82 // @description Subset of RES features I like. // @include https://*.reddit.com/* // @version 1.5.8 // @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", ".subreddit .usertext .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", ".link .md :not(pre) > code", ".link .md pre"], 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", ".subreddit .usertext .md", ".link .md :not(pre) > code", ".link .md pre"], 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.replace(/[^\/]*\.twitter.com/, "twitter.com"); 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 result = document.createElement("div"); var domTweet = html.querySelector("div.tweet"); if (domTweet) { var resultUser = document.createElement("div"); resultUser.textContent = domTweet.dataset.screenName + " (" + domTweet.dataset.name + ")"; result.appendChild(resultUser); } var domDescription = html.querySelector('meta[property="og:description"]'); if (domDescription) { var resultDescription = document.createElement("div"); resultDescription.textContent = domDescription.getAttribute("content"); result.appendChild(resultDescription); } var domVideo = html.querySelector('meta[property="og:video:url"]'); var domPicture = html.querySelector('meta[property="og:image:user_generated"]'); if (domVideo) { var resultVideo = document.createElement("iframe"); resultVideo.width = html.querySelector('meta[property="og:video:width"]').getAttribute("content"); resultVideo.height = html.querySelector('meta[property="og:video:height"]').getAttribute("content"); resultVideo.src = domVideo.getAttribute("content"); resultVideo.cssText = "border: none"; result.appendChild(resultVideo); } else if (domPicture) { var resultPicture = document.createElement("img"); resultPicture.src = html.querySelector('meta[property="og:image"]').getAttribute("content"); result.appendChild(resultPicture); } resolve(result); } 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.CustomPromise = 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 domTitle = html.querySelector('meta[property="og:title"]'); var domBody = html.querySelector('meta[property="og:description"]'); var domDate = html.querySelector('meta[property="article:published_time"]'); if (!domTitle && !domBody) { Redditmod.OtherPromise(url).then(resolve, reject); return; } var result = document.createElement("div"); result.cssText = "max-width: 33%"; var resultTitleBox = document.createElement("h3"); var resultTitle = document.createElement("a"); resultTitle.textContent = domTitle.getAttribute("content"); resultTitle.href = theUrl; resultTitle.target = "_BLANK"; resultTitle.cssText = "font-style: italic"; resultTitleBox.appendChild(resultTitle); result.appendChild(resultTitleBox); if (domDate) { var resultDate = document.createElement("div"); resultDate.textContent = domDate.getAttribute("content"); result.appendChild(resultDate); } var resultBody = document.createElement("div"); resultBody.textContent = domBody.getAttribute("content"); result.appendChild(resultBody); resolve(result); } catch (error) { reject("Error (" + error + "): Failed to load page ", theUrl); } } }); }); } 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 } else if (url.host === "v.redd.it") { return null; } // 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.CustomPromise(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 }; })();