NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Something Awful Image Fixes // @namespace SA // @description Smarter image handling on the Something Awful forums. // @include http://forums.somethingawful.com/* // @version 1.3.4 // @grant GM_openInTab // @grant GM_getValue // @grant GM_setValue // @run-at document-end // @icon http://forums.somethingawful.com/favicon.ico // ==/UserScript== "use strict"; var Config = { createIssue: 'https://github.com/psychoticmeow/Something-Awful-Image-Fixes/issues/new', imgurClientId: 'b0bde100257fa60', imgurMaxGifSize: 3 * 1024 * 1024, loadingIndicator: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAAAPFBMVEU5Ojg+QD1FRkRPUE5aW1lkZmNucG16e3iBg4CIioeTlZKgop+srqu4urfAwr/GyMXR09Dd4Nzo6uf09/OAsyLcAAAB0klEQVRIx72V63KEIAyFhUAioKL2/d+14SbqgtPt7Gx+dfB8HHLZdBg+E+gX/Rbg13X5g0yAyH+tHOVQiZ5ezn6Sd0D6zcsOQN57ewfctm1jN1MOdQUU6zfspTAx4K7AzPqlm4QKFvoMUDB4KLBlYBYVECvr3UNdYWFiDMIE2GAAjXqSzs8cGVigNA6C3uQEyUBNdp5dqo6YmaAyGob1a7pKr/u+luRhDmGhlPZIEktJYd5DwMkhBIUbyFF96ziFnkkb5dVhkCYRU7OAtCW9O08IuIQ0emqSfFG3cx3fZV+B+PyVGoNBXYfNtodDom59QAPDd0O5KYdTnR/W/pNjD7kfeibawKFn4j/AW0/CL9dLYvNNAO0+C3TONcYVELGFaMt611hZGkPcpwOMi9Fx4NDysgSS3Db3biawfhQ26TGcaDq5KK3i/s4mxxKIcoqeylp71AqICIaKyJNDnnlhGGALGEc+UAxQulZy7tVa6nIpst7yRaMxYQEGQD21Q9pkMBiOIVvIh14T642oQLR4WN5gS8oZiGlT9ycdM069LsCADHRnOpQ0bdgKyCcLzfp82wFwI2uhXmpkUsYXQCBh93+ckOVTBU6HT5Ea90ak0fhI/ALpBRsilkfZxwAAAABJRU5ErkJggg==" }; var Util = { /** * Initialise the page, strip out any assets that we will load. */ initialise: function(target) { var assets = []; // Remove content images: var images = document.querySelectorAll('td.postbody img'); for (var index in images) { var image = images[index]; if (typeof image !== 'object') continue; var src = image.getAttribute('src'); // Exclude smilies: if (!/somethingawful[.]com[/](images[/]smilies|forumsystem[/]emoticons)[/]/.test(src)) { var placeholder = document.createElement('span'); // Replace the image with a placeholder: placeholder.setAttribute('class', 'saif-pending'); image.parentNode.replaceChild(placeholder, image); // Remove a parent link element: if ( placeholder.parentNode instanceof HTMLAnchorElement && placeholder.parentNode.children.length === 1 && !!placeholder.parentNode.parentNode ) { placeholder.parentNode.parentNode.replaceChild( placeholder, placeholder.parentNode ); } // Create asset: if (/i\.imgur\.com/.test(src)) { assets.push(new ImgurAsset(placeholder, src)); } else if (/staticflickr\.com\//.test(src)) { assets.push(new FlickrAsset(placeholder, src)); } else { assets.push(new GenericAsset(placeholder, src)); } } } // Reload other images: var images = document.querySelectorAll('img'); for (var index in images) { var image = images[index]; if (!image.parentNode) continue; image.parentNode.replaceChild(image.cloneNode(true), image); } // Reload embedded iframes: var iframes = document.querySelectorAll('td.postbody iframe'); for (var index in iframes) { var iframe = iframes[index]; if (!iframe.parentNode) continue; iframe.parentNode.replaceChild(iframe.cloneNode(true), iframe); } // Fix post table styles: var posts = document.querySelectorAll('table.post'); for (var index in posts) { var post = posts[index]; if (typeof post !== 'object') continue; post.style.tableLayout = 'fixed'; } Util.build(target, assets); }, /** * Begin loading assets from the start of the document * until and including the windows viewport. */ build: function(target, assets) { var offset = window.scrollY + window.innerHeight, queue = [], scroll; // Keep the window scrolled to the target: if (!!target) { offset = Util.getElementOffset(target) + window.innerHeight; scroll = setInterval(function() { window.scrollTo(0, Util.getElementOffset(target)); }, 100); } // Initialise all elements up until the offset: for (var index in assets) { var asset = assets[index]; if (asset.getOffset() < offset) { queue.push(asset.build()); } } // Wait for the queued assets to render: Promise.all(queue).then(function() { var queue = []; // Scroll to the URL target: if (!!target) { clearInterval(scroll); window.scrollTo(0, Util.getElementOffset(target)); } // Load remaining assets: for (var index in assets) { var asset = assets[index]; if (!asset.rendered) { asset.build(); } } }); }, /** * Create a formatted error message. */ createError: function(message, data) { var wrapper = document.createElement('div'), title = document.createElement('p'), footer = document.createElement('p'), link = document.createElement('a'), raw = document.createElement('pre'); wrapper.setAttribute('class', 'saif-error'); title.textContent = 'Sorry, an error occured while making things pretty:'; wrapper.appendChild(title); // Make the data copy+paste safe: data = btoa(JSON.stringify(data)); data = data.match(/.{1,76}/g).join("\n"); data = message + "\n\n" + data; raw.textContent = data; raw.style.whiteSpace = 'pre-wrap'; wrapper.appendChild(raw); footer.appendChild(document.createTextNode('If this problem persists, please ')); data = "```\n" + data + "\n```"; data = "**What was the error?**\n" + data; data = "**Where did this happen?**\n" + window.location + "\n\n" + data; data = "**What did you experience?**\n\n\n" + data; link.textContent = 'report this issue'; link.setAttribute('href', Config.createIssue + '?body=' + encodeURIComponent(data)); link.setAttribute('target', '_blank'); footer.appendChild(link); footer.appendChild(document.createTextNode(' on our issue tracker.')); wrapper.appendChild(footer); return wrapper; }, /** * Create a simple message. */ createMessage: function(message) { var wrapper = document.createElement('div'), title = document.createElement('p'); wrapper.setAttribute('class', 'saif-error'); title.textContent = message; wrapper.appendChild(title); return wrapper; }, /** * Create a simple image element from a given source URL. */ createImage: function(source) { return function(callback) { var image = document.createElement('img'); image.addEventListener('load', function() { callback(true, image); }); image.addEventListener('error', function() { callback(false, image); }); // Set image source: image.setAttribute('src', source); return image; }; }, /** * Inject a stylesheet into the page head. */ createStyle: function(css) { var head = document.querySelectorAll('head')[0], style = document.createElement('style'); style.textContent = css; head.appendChild(style); }, /** * Create a video element from a list of source URLs with media types. */ createVideo: function(sources) { return function(callback) { var video = document.createElement('video'); // Set attributes to ensure gif style playback: video.setAttribute('preload', 'auto'); video.setAttribute('autoplay', 'autoplay'); video.setAttribute('muted', 'muted'); video.setAttribute('loop', 'loop'); video.setAttribute('webkit-playsinline', 'webkit-playsinline'); // Listen for success: video.addEventListener('loadeddata', function() { callback(true, video); }); // Set the video sources: for (var index in sources) { var source = document.createElement('source'); source.setAttribute('src', sources[index]); source.setAttribute('type', ( /[.]webm$/.test(sources[index]) ? 'video/webm' : 'video/mp4' )); // Listen for failure: if (index == sources.length - 1) { source.addEventListener('error', function() { callback(false, video, source); }); } video.appendChild(source); } }; }, /** * Calculate the offset from the top of the page to the * top of the given element. */ getElementOffset: function(element) { var offset = 0; while (element.offsetParent) { offset += element.offsetTop; element = element.offsetParent; } return offset; } }; function Deferred() { try { this.resolve = null; this.reject = null; this.promise = new Promise(function(resolve, reject) { this.resolve = resolve; this.reject = reject; }.bind(this)); Object.freeze(this); } catch (error) { throw new Error('Promise/Deferred is not available'); } } var Asset = (function() { function Asset(placeholder, source) { if (!!placeholder) { var self = this; self.placeholder = placeholder; self.source = source; self.wrapper = document.createElement('span'); self.link = document.createElement('a'); self.wrapper.setAttribute('class', 'saif-wrapper'); self.link.setAttribute('target', '_blank'); self.wrapper.appendChild(self.link); } }; Asset.prototype.getOffset = function() { var self = this; if (!!self.placeholder.parentNode) { return Util.getElementOffset(self.placeholder); } else if (!!self.wrapper.parentNode) { return Util.getElementOffset(self.wrapper); } return 0; }; Asset.prototype.build = function() { var self = this; if (!self.rendered) { self.rendered = new Deferred(); self.render(self.source); } return self.rendered.promise; }; Asset.prototype.visible = function() { var deferred = new Deferred(), chromeSucks = false, self = this; var scroll = function() { if (chromeSucks) return; var offset = self.getOffset(), max = window.scrollY + (window.innerHeight * 2); if (max > offset) { chromeSucks = true; window.removeEventListener('scroll', scroll); deferred.resolve(self); } }; window.addEventListener('scroll', scroll); return deferred.promise; }; Asset.prototype.setLink = function(href) { var self = this; self.link.setAttribute('href', href); }; Asset.prototype.setContent = function(content) { var self = this; self.link.appendChild(content); if (!!self.placeholder.parentNode) { self.placeholder.parentNode.replaceChild(self.wrapper, self.placeholder); } self.rendered.resolve(self); }; return Asset; })(); var FlickrAsset = (function() { function FlickrAsset(placeholder, source) { this.parent.constructor.call(this, placeholder, source); }; FlickrAsset.prototype = new Asset(); FlickrAsset.prototype.parent = Asset.prototype; FlickrAsset.prototype.constructor = FlickrAsset; FlickrAsset.prototype.render = function(source) { var bits = /^(.+?\.com\/.+?\/.+?_.+?)(_[sqtmn-zcbhko])?\.(.+?)$/.exec(source), self = this; // Create an image: if (bits) { var source = bits[1] + '_b.' + bits[3].toLowerCase(); Util.createImage(source)(function(success, element) { if (success) { self.setLink(source); self.setContent(element); } else { self.setContent( Util.createError('Could not load image.', { url: source }) ); } }); } // The source was invalid: else { self.setContent( Util.createError('Could not parse URL.', { url: source }) ); } }; return FlickrAsset; })(); var GenericAsset = (function() { function GenericAsset(placeholder, source) { this.parent.constructor.call(this, placeholder, source); }; GenericAsset.prototype = new Asset(); GenericAsset.prototype.parent = Asset.prototype; GenericAsset.prototype.constructor = GenericAsset; GenericAsset.prototype.render = function(source) { var self = this; Util.createImage(source)(function(success, element) { if (success) { self.setLink(source); self.setContent(element); } else { self.setContent( Util.createError('Could not load image.', { url: source }) ); } }); }; return GenericAsset; })(); var ImgurAsset = (function() { function ImgurAsset(placeholder, source) { var bits = /\/(.{5}|.{7})[sbtmlh]?\.(jpg|png|gif)/i.exec(source), self = this; self.parent.constructor.call(self, placeholder, source); self.prefetched = new Deferred(); // Update the source: if (bits) { self.prefetch(bits[1], 'https://api.imgur.com/3/image/' + bits[1]); } // Could not parse the image source: else { self.prefetched.reject( Util.createError('Could not parse URL.', { url: self.source }) ); } }; ImgurAsset.prototype = new Asset(); ImgurAsset.prototype.parent = Asset.prototype; ImgurAsset.prototype.constructor = ImgurAsset; ImgurAsset.prototype.build = function() { var self = this; if (!self.rendered) { self.rendered = new Deferred(); self.prefetched.promise.then( function(data) { self.render(data); }, function(content) { self.setContent(content); } ); } return self.rendered.promise; }; ImgurAsset.prototype.prefetch = function(id, url) { var request = new XMLHttpRequest(), data = GM_getValue('imgur-data-' + id), self = this; // Use stored data: if (!!data) { self.prefetched.resolve(JSON.parse(data)); } // Load and then store data: else { request.addEventListener('load', function() { try { var data = JSON.parse(request.response); if (!data || !data.success || !data.data) { throw data; } } catch (error) { // Image not found: if (!!error.status && error.status === 404) { self.prefetched.reject( Util.createMessage('The image you are requesting does not exist or is no longer available.') ); } // Generic error: else { self.prefetched.reject( Util.createError('Invalid response from imgur.com.', { url: url, result: error }) ); } return; } // Store date for later: GM_setValue('imgur-data-' + id, JSON.stringify(data.data)); self.prefetched.resolve(data.data); }); request.addEventListener('error', function() { self.prefetched.reject( Util.createError('Could not access imgur.com.', { url: url }) ); }); request.open('GET', url, true); request.setRequestHeader('Authorization', 'Client-ID ' + Config.imgurClientId); request.send(); } }; ImgurAsset.prototype.render = function(data) { var self = this, video, link; // Has a video page: if (!!data.gifv) { link = data.gifv; } // Link to the raw image: else { link = data.link; } // Show a video if (data.animated && Config.imgurMaxGifSize < data.size) { if (!!data.webm && !!data.mp4) { video = Util.createVideo([data.webm, data.mp4]); } else if (!!data.webm) { video = Util.createVideo([data.webm]); } else if (!!data.mp4) { video = Util.createVideo([data.mp4]); } } var image = function() { return Util.createImage(data.link)(function(success, element) { if (success) { self.setLink(link); self.setContent(element); } else { self.setContent( Util.createError('Could not load image.', { url: link }) ); } }); }; if (!!video) video(function(success, element) { if (success) { self.setLink(link); self.setContent(element); } else image(); }); else image(); }; return ImgurAsset; })(); try { Util.createStyle(` @keyframes saif-spinner { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } span.saif-pending { background: #3b3b3b; border-radius: 32px; display: inline-block; height: 32px; overflow: hidden; width: 32px; } span.saif-pending:before { animation-duration: 1s; animation-name: saif-spinner; animation-iteration-count: infinite; animation-timing-function: steps(12, end); background: url(` + Config.loadingIndicator + `) no-repeat center; background-size: 24px 24px; content: ''; display: block; height: 32px; width: 32px; } span.saif-wrapper { display: inline-block; position: relative; margin: 5px 0; max-width: 100%; } span.saif-wrapper img, span.saif-wrapper video { max-height: 1000px; max-width: 100%; opacity: 1; vertical-align: bottom; } span.saif-wrapper div.saif-error { background: #3b3b3b; border-radius: 8px; color: #ffffff; padding: 1px 1em; } span.saif-wrapper div.saif-error pre { color: #cccccc; font-size: 0.9em; line-height: 1.2; } span.saif-wrapper div.saif-error a { color: #ffffff; } span.saif-wrapper a.saif-link { background: hsla(0, 0%, 10%, 0.7); color: #ffffff; cursor: pointer; display: none; font-size: 0.75em; left: 0; line-height: 1; padding: 5px; position: absolute; right: 0; text-decoration: none; top: 0; z-index: 1; } span.saif-wrapper:hover a.saif-link { display: block; } `); // Prevent images from loading: window.stop(); // Redirect the page: if (document.querySelectorAll('meta[http-equiv=refresh]').length) { var rule = document.querySelectorAll('meta[http-equiv=refresh]')[0].getAttribute('content'); if (/URL=(.+)$/.test(rule)) { window.location = /URL=(.+)$/.exec(rule)[1]; } } // Jump to appropriate place on page: else if (!!window.location.hash && document.querySelectorAll(window.location.hash).length) { Util.initialise(document.querySelectorAll(window.location.hash)[0]); } // Load the page normally: else { Util.initialise(); } } catch (e) { console.log("Exception: " + e.name + " Message: " + e.message); }