Something Awful Image Fixes

Smarter image handling on the Something Awful forums.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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);
}