Something Awful Image Fixes

Smarter image handling on the Something Awful forums.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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);
}