// ==UserScript==
// @name Something Awful Image Fixes
// @namespace SA
// @description Smarter image handling on the Something Awful forums.
// @include http://forums.somethingawful.com/*
// @version 1.2.0
// @grant GM_openInTab
// @run-at document-end
// @icon http://forums.somethingawful.com/favicon.ico
// ==/UserScript==
var Util = {
assetsLoaded: false,
assetsLoading: 0,
/**
* Initialise the page, strip out any assets that we will load.
*/
initialise: function(target) {
// 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');
placeholder.setAttribute('data-saif-pending', 'yes');
placeholder.saifCreate = src;
image.parentNode.replaceChild(placeholder, image);
}
}
// Remove other images:
var images = document.querySelectorAll('img');
for (var index in images) {
var image = images[index];
if (!image.parentNode) continue;
var placeholder = document.createElement('span');
placeholder.setAttribute('data-saif-pending', 'yes');
placeholder.saifClone = image;
image.parentNode.replaceChild(placeholder, image);
}
// Remove embedded videos:
var iframes = document.querySelectorAll('td.postbody iframe');
for (var index in iframes) {
var iframe = iframes[index];
if (!iframe.parentNode) continue;
var placeholder = document.createElement('span');
placeholder.setAttribute('data-saif-pending', 'yes');
placeholder.saifClone = iframe;
iframe.parentNode.replaceChild(placeholder, 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.beginLoading(target);
},
/**
* Begin loading assets from the start of the document
* until and including the windows viewport.
*/
beginLoading: function(target) {
var offset = window.scrollY + window.innerHeight;
if (!!target) {
offset = Util.getElementOffset(target) + window.innerHeight
}
// Initialise all elements up until the offset:
var placeholders = document.querySelectorAll('[data-saif-pending]'),
queue = [];
for (var index in placeholders) {
var placeholder = placeholders[index];
if (typeof placeholder !== 'object') continue;
if (Util.getElementOffset(placeholder) < offset) {
queue.push(placeholder);
}
}
for (var index in queue) {
Util.createElement(queue[index]);
}
// Wait until everything initialised thus far is loaded:
if (!!target) {
Util.waitForReady(function() {
// Scroll to the target element:
window.scrollTo(0, Util.getElementOffset(target));
// Resume loading of images and videos:
Util.resumeLoading();
});
}
// No reason to wait:
else {
Util.resumeLoading();
}
},
/**
* Resume loading assets not handled by `Util.beginLoading`
* as they become visible in the windows viewport.
*/
resumeLoading: function() {
var placeholders = document.querySelectorAll('[data-saif-pending]');
for (var index in placeholders) {
var placeholder = placeholders[index];
Util.waitForVisibility(placeholder, function(placeholder) {
Util.createElement(placeholder);
});
}
},
/**
* Create an asset element from a placeholder.
*/
createElement: function(placeholder) {
if (!placeholder.parentNode) return;
// No processing needs to be done:
if (!!placeholder.saifClone) {
var element = placeholder.saifClone.cloneNode(true);
// Track the loading of this image:
if (element instanceof HTMLImageElement) {
Util.trackLoadState(element, ['load']);
}
placeholder.parentNode.replaceChild(element, placeholder);
}
// Process the source of this image:
else if (!!placeholder.saifCreate) {
var src = placeholder.saifCreate;
if (/i\.imgur\.com/.test(src)) {
Util.createImgur(placeholder, src);
}
else if (/staticflickr\.com\//.test(src)) {
Util.createFlickr(placeholder, src);
}
else {
Util.createImage(placeholder, src);
}
}
},
/**
* Create an empty element indicating of failure.
*/
createEmpty: function(placeholder) {
var span = document.createElement('span');
span.setAttribute('data-saif-empty', 'yes');
placeholder.parentNode.replaceChild(span, placeholder);
},
/**
* Create a simple image element from a given source URL.
*/
createImage: function(placeholder, src, href) {
var wrapper = document.createElement('span');
wrapper.setAttribute('class', 'saif-wrapper');
// Create image element:
var image = document.createElement('img');
// Track the loading of this image:
Util.trackLoadState(image, ['load']);
// Append the image to the page when it is loaded:
image.addEventListener('load', function() {
if (!!href) {
var link = document.createElement('a');
link.setAttribute('href', href);
link.appendChild(image);
wrapper.appendChild(link);
}
else {
wrapper.appendChild(image);
}
if (!!placeholder.parentNode) {
placeholder.parentNode.replaceChild(wrapper, placeholder);
}
});
// Set image source:
image.setAttribute('src', src);
},
/**
* Create a video element from a list of source URLs with media types.
*/
createVideo: function(placeholder, src, href, sources) {
var wrapper = document.createElement('span');
wrapper.setAttribute('class', 'saif-wrapper');
// Create video element:
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');
var action = document.createElement('a');
action.setAttribute('class', 'saif-link');
action.setAttribute('target', '_blank');
action.textContent = 'See original';
action.addEventListener('click', function(event) {
event.stopPropagation();
event.preventDefault();
wrapper.setAttribute('class', 'saif-wrapper hide');
wrapper.removeChild(action);
video.pause();
Util.createImage(wrapper, src, href);
});
wrapper.appendChild(action);
// Video has loaded, insert it or a fallback:
video.addEventListener('loadeddata', function() {
if (
(video.videoWidth < 75 && video.videoHeight < 100)
|| (video.videoHeight < 75 && video.videoWidth < 100)
) {
video.pause();
Util.createImage(placeholder, src, href);
}
else {
var link = document.createElement('a');
link.setAttribute('href', href);
link.appendChild(video);
wrapper.appendChild(link);
if (!!placeholder.parentNode) {
placeholder.parentNode.replaceChild(wrapper, placeholder);
}
}
});
// Track the loading of this video:
Util.trackLoadState(video, ['loadeddata', 'error']);
// Add media sources:
for (var index in sources) {
var source = document.createElement('source');
source.setAttribute('src', sources[index][0]);
source.setAttribute('type', sources[index][1]);
video.appendChild(source);
}
},
createStyle: function(css) {
var head = document.querySelectorAll('head')[0],
style = document.createElement('style');
style.textContent = css;
head.appendChild(style);
},
/**
* Create an imgur image or video from a given source URL.
*/
createImgur: function(placeholder, src) {
var bits = /\/(.{5}|.{7})[hls]?\.(jpg|png|gif)/i.exec(src);
// Could not parse the image:
if (bits) {
var identity = bits[1],
extension = bits[2].toLowerCase();
// Is a video:
if ('gif' === extension) {
Util.createVideo(
placeholder,
'//i.imgur.com/' + identity + '.' + extension,
'//i.imgur.com/' + identity + '.' + extension,
[
['//i.imgur.com/' + identity + '.webm', 'video/webm'],
['//i.imgur.com/' + identity + '.mp4', 'video/mp4']
]
);
}
// Is an image:
else {
Util.createImage(
placeholder,
'//i.imgur.com/' + identity + 'h.' + extension,
'//i.imgur.com/' + identity + '.' + extension
);
}
}
// The source was invalid:
else {
Util.createEmpty(placeholder);
}
},
/**
* Create a flickr image from a given source URL.
*/
createFlickr: function(placeholder, src) {
var bits = /^(.+?\.com\/.+?\/.+?_.+?)(_[omstzb])?\.(.+?)$/.exec(src),
location,
extension;
// Create an image:
if (bits) {
var location = bits[1],
extension = bits[3].toLowerCase();
Util.createImage(
placeholder,
location + '_b.' + extension,
location + '_b.' + extension
);
}
// The source was invalid:
else {
Util.createEmpty(placeholder);
}
},
/**
* 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;
},
/**
* Style an element so that it cannot break out of the post table.
*/
setElementStyles: function(element) {
element.style.display = 'inline-block';
element.style.marginBottom = '5px';
element.style.marginTop = '5px';
element.style.maxWidth = '100%';
},
/**
* Attach events to count the number of currently loading assets.
*/
trackLoadState: function(element, eventNames) {
if (Util.assetsLoaded) return;
Util.assetsLoading++;
for (var index in eventNames) {
element.addEventListener(eventNames[index], Util.trackReadyState);
}
},
/**
* The attached event handler for `Util.trackLoadState`.
*/
trackReadyState: function(event) {
if (Util.assetsLoaded) return;
Util.assetsLoading--;
if (0 === Util.assetsLoading) {
Util.assetsLoaded = true;
}
},
/**
* Wait for all of the assets loaded in `Util.beginLoading`
* to complete.
*/
waitForReady: function(callback) {
var wait = setInterval(function() {
if (Util.assetsLoaded) {
clearInterval(wait);
callback();
}
}, 1);
},
/**
* Wait for the user to scroll within two pages of an element
* and then call the callback.
*/
waitForVisibility: function(element, callback) {
var chromeSucks = false;
var scroll = function() {
if (chromeSucks) return;
var offset = Util.getElementOffset(element),
max = window.scrollY + (window.innerHeight * 2);
if (max > offset) {
chromeSucks = true;
window.removeEventListener('scroll', scroll);
callback(element);
}
};
scroll();
window.addEventListener('scroll', scroll);
}
};
try {
var offset = window.outerHeight;
Util.createStyle("\
@-webkit-keyframes saifProgressSlider {\
0% { background: #3b3b3b; }\
100% { background: #3b3b3b; }\
}\
@keyframes saifProgressSlider {\
0% { background-position: 0px 0px; }\
100% { background-position: 16px 0px; }\
}\
span.saif-wrapper {\
display: inline-block;\
position: relative;\
margin: 5px 0;\
max-width: 100%;\
}\
span.saif-wrapper img,\
span.saif-wrapper video {\
max-width: 100%;\
opacity: 1;\
vertical-align: bottom;\
}\
span.saif-wrapper.hide {\
background: repeating-linear-gradient(45deg, #444444 0px, #444444 8px, #3b3b3b 8px, #3b3b3b 16px) scroll 0% 0% / 300% 300%;\
-webkit-animation: saifProgressSlider 60s linear infinite;\
animation: saifProgressSlider 1s linear infinite;\
}\
span.saif-wrapper.hide video {\
transition: opacity 0.5s ease;\
opacity: 0;\
}\
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);
}