MassiveFap

A complete ImageFap.com gallery conversion script featuring customization options and multiple viewing modes.

// ==UserScript==
// @name		MassiveFap
// @author		Ryan Thaut
// @description	A complete ImageFap.com gallery conversion script featuring customization options and multiple viewing modes.
// @namespace	https://greasyfork.org/users/408380
// @include		https://*imagefap.com/*
// @version		1.6.2
// @grant		GM_getValue
// @grant		GM_setValue
// @grant		GM_registerMenuCommand
// @grant		GM_addStyle
// ==/UserScript==



/*	===== Integration =====
	This is where the initial/basic eventListeners are added.
*/
window.addEventListener('load', init, false);
document.addEventListener('DOMContentLoaded', init, false);



/*	===== Global Variables =====
	Changing these is a terrible idea; they are NOT for configuration
*/
var fullscreen = false;
var loaded = false;
var images = [];
var head, body, title, addFavLink;
var author = {name: '', url: ''};
var activeImage = (parseInt(getHashParam('image'), 10) - 1) || 0;
var totalImages = 0;
var hotkeys = [
		{
			action:	displayHelp,
			codes:	[191],
			keys:	'?',
			label:	'Display Help',
			modif:	{altKey: false, ctrlKey: false, metaKey: false, shiftKey: true}
		},
		{
			action:	displayAbout,
			codes:	[65],
			keys:	'a',
			label:	'Display About',
			modif:	{altKey: false, ctrlKey: false, metaKey: false, shiftKey: false}
		},
		{
			action:	toggleFullScreen,
			codes:	[70],
			keys:	'f',
			label:	'Toggle Full Screen',
			modif:	{altKey: false, ctrlKey: false, metaKey: false, shiftKey: false}
		},
		{
			action:	switchGalleryMode,
			codes:	[71],
			keys:	'g',
			label:	'Switch Gallery Mode',
			modif:	{altKey: false, ctrlKey: false, metaKey: false, shiftKey: false}
		},
		{
			action:	changeSettings,
			codes:	[83],
			keys:	's',
			label:	'Change Settings',
			modif:	{altKey: false, ctrlKey: false, metaKey: false, shiftKey: false}
		},
		{
			action:	toggleThumbs,
			codes:	[84],
			keys:	't',
			label:	'Change Thumbnails',
			modif:	{altKey: false, ctrlKey: false, metaKey: false, shiftKey: false}
		},
		{
			action:	hideDialog,
			codes:	[27],
			keys:	'esc',
			label:	'Hide Dialog Window',
			modif:	{altKey: false, ctrlKey: false, metaKey: false, shiftKey: false}
		},
		{
			action:	prevImage,
			codes:	[37, 38],
			keys:	'← ↑',
			label:	'Previous Image',
			modif:	{altKey: false, ctrlKey: false, metaKey: false, shiftKey: false}
		},
		{
			action:	nextImage,
			codes:	[39, 40],
			keys:	'→ ↓',
			label:	'Next Image',
			modif:	{altKey: false, ctrlKey: false, metaKey: false, shiftKey: false}
		},
		{
			action:	toggleAutoplay,
			codes:	[32],
			keys:	'[space]',
			label:	'Start/Stop Autoplay',
			modif:	{altKey: false, ctrlKey: false, metaKey: false, shiftKey: false}
		}
	];
var settings = {
		'autoplay':	{
			name:	'autoplayDelay',
			label:	'Auto Play Delay',
			hint:	'in seconds',
			type:	'integer',
			size:	3,
			min:	0,
			max:	null,
			def:	2
		},
		'inifiteScrolling':		{
			name:	'inifiteScrolling',
			label:	'Infinite Scrolling',
			type:	'boolean',
			def:	true
		},
		'mode':		{
			name:	'galleryMode',
			label:	'Gallery Mode',
			type:	'select',
			opts:	['scrolling', 'slideshow'],
			def:	'scrolling'
		},
		'pagination':	{
			name:	'imageLimit',
			label:	'Pagination Limit',
			hint:	'use <b>0</b> to disable',
			type:	'integer',
			size:	3,
			min:	0,
			max:	null,
			def:	0
		},
		'preloading':	{
			name:	'preloadingEnabled',
			label:	'Image Preloading',
			type:	'boolean',
			def:	true
		},
		'theme':		{
			name:	'theme',
			label:	'Gallery Theme',
			type:	'select',
			opts:	['default', 'classic', 'green', 'blue'],
			def:	'default'
		},
		'thumbnails':	{
			name:	'showThumbs',
			label:	'Show Thumbnails',
			type:	'boolean',
			def:	true
		},
		'thumbnailsSize':	{
			name:	'thumbnailsSize',
			label:	'Thumbnails Size',
			type:	'select',
			opts:	['small', 'medium', 'large'],
			def:	'medium'
		}
	};

// objects for features that need multiple settings and calculated properties
var autoplay = {
		active: false,
		count:	parseInt(GM_getValue(settings.autoplay.name, settings.autoplay.def), 10),
		delay:	parseInt(GM_getValue(settings.autoplay.name, settings.autoplay.def), 10),
		paused: false,
		timer:	undefined
	};
var pagination = {
		append: GM_getValue(settings.inifiteScrolling.name, settings.inifiteScrolling.def),
		active:	false,
		limit:	parseInt(GM_getValue(settings.pagination.name, settings.pagination.def), 10),
		page:	parseInt(getHashParam('page'), 10) || 1
	};
var preloading = {
		active: GM_getValue(settings.preloading.name, settings.preloading.def),
		done:	false,
		pos:	activeImage
	};



/*	===== Core Functions =====
	Where the magic happens...
*/

/**
 * Crawls the normal gallery page and finds all thumbnail images
 * @return	Array	locations of all thumbnail images
 */
function findImages() {
	var imgs = document.getElementById('gallery').getElementsByTagName('img');
	var thumbRegex = /(.*\/images\/thumb\/.*)\/(.*)$/i;

	var count = 0;
	var image;
	var ret = [];
	for (var i = 0; i < imgs.length; i++) {
		if (thumbRegex.test(imgs[i].src)) {
            if (imgs[i].src.indexOf("?") > 0) {
                var isrc = imgs[i].src.substring(0, imgs[i].src.indexOf("?"));
            }
			image = {
				id: 	imgs[i].src.split('/').pop().split('.')[0],
				pos: 	count++,
				thumb: 	imgs[i].src,
                full: 'https://www.imagefap.com/photo/'+imgs[i].src.split('/').pop().split('.')[0]+'/'
				//full: 	isrc.replace('thumb', 'full').replace("https", "http").replace("cdn.imagefap.com", "109.201.130.54")
			};
			ret.push(image);
		}
	}

	totalImages = ret.length;
	return ret;
}

/**
 * Sets the global variables needed for pagination in other functions
 */
function initPagination() {
	initSetting('pagination');

	// reset pagination object properties to default
	pagination = {
		append: settings.inifiteScrolling.value,
		active:	(settings.mode.value === 'slideshow'),
		limit:	parseInt(settings.pagination.value, 10),
		page:	parseInt(getHashParam('page'), 10) || 1
	};

	if ((pagination.limit <= 0) || (settings.mode.value === 'slideshow')) {
		// in slideshow mode pagination is handled as if it is disabled
		pagination.active = false;
		pagination.limit = totalImages;
		pagination.page = 1;
	} else {
		pagination.active = true;
		if (!pagination.page || (pagination.page < 1))
			pagination.page = 1;
	}

	// ensure the user is on the correct page
	var page = findImagePage();
	if (page !== pagination.page) {
		setHashParam('page', page);
		pagination.page = page;
	}
}

/**
 * Finds the page number that the active image should be on
 * @param	Int		(Optional) The number of the image to find (default: value of activeImage internal variable)
 * @return	Int		The page number containing the active image
 */
function findImagePage(pos) {
	if ((typeof activeImage === 'undefined') || !activeImage || (activeImage <= 0))
		return 1;

	return parseInt(((activeImage / pagination.limit) + 1), 10);
}

/**
 * Returns the images that will be used on the current page
 * @param	Array	Objects representing all images from the original gallery
 * @return	Array	Multi-dimensional array of objects representing all images on each page
 */
function paginateImages(imgs) {
	// if images have been paginated previously, they must first be un-paginated
	if (typeof imgs[0] === 'object' && typeof imgs[0][0] === 'object')
		imgs = resetImages(imgs);

	var page = 0;
	var ret = [];
	for (var i = 0; i < imgs.length; i++) {
		if (typeof ret[page] === 'undefined')
			ret[page] = [];
		ret[page].push(imgs[i]);

		if (((i + 1) % pagination.limit) === 0)
			page++;
	}
	return ret;
}

/** Flattens a paginated multi-dimensional array of images
 * @param	Array	Multi-dimensional array of objects representing all images on multiple page
 * @return	Array	Multi-dimensional array of objects representing all images on one page
 */
function resetImages(imgs) {
	var ret = [];
	for (var i = 0; i < imgs.length; i++) {
		for (var j = 0; j < imgs[i].length; j++) {
			ret.push(imgs[i][j]);
		}
	}
	// if the supplied array was only 1-dimensional, then the new array will be empty
	if (ret.length === 0)
		ret = imgs;
	return ret;
}

/**
 * Loads the next "page" of images in Infinite Scrolling mode
 * Updates the position text and the pagination links
 */
function loadNextPage() {
	if (pagination.page < images.length) {
		pagination.page++;
		showNotification('Loading images from page ' + pagination.page);

		updatePosition(undefined, (pagination.limit * pagination.page), undefined, undefined);
		updatePagination(pagination.page);

		populateScrollingGallery(images[(pagination.page - 1)], false);
		populateThumbnails(images[(pagination.page - 1)], false);

		setHashParam('page', pagination.page);
	} else {
		var loader = document.getElementById('loader');
		if (loader)
			loader.parentNode.removeChild(loader);
	}
}

/**
 * Generates HTML for the help dialog
 * @return	String	HTML to be placed in the dialog
 */
function getAbout() {
	var about = '';
	about += '<p>' + GM_info.script.name + ' v' + GM_info.script.version + '. Automatic script updates are ' + ((GM_info.scriptWillUpdate) ? 'enabled' : 'disabled') + '. </p>';
	about += '<p>' + GM_info.script.description + '</p>';

	return about;
}

/**
 * Generates HTML for the help dialog
 * @return	String	HTML to be placed in the dialog
 */
function getHelp() {
	var help = '';
	help += '<table>';
	help += '<tr><th></th><th>Hotkeys</th></tr>';
	for (var h in hotkeys)
		help += '<tr><td class="key"><b>' + hotkeys[h].keys + '</b></td><td class="command">' + hotkeys[h].label + '</td></tr>';

	help += '</table>';

	return help;
}

/**
 * Generates the DOM objects needed for the thumbnail images
 * @param	Array	The objects of all images to be displayed
 * @param	Bool	(Optional) If the existing thumbnails should be removed (default: false)
 */
function populateThumbnails(imgs, reset) {
	reset = (typeof reset === "undefined") ? false : reset;

	var thumbs = document.getElementById('thumbnails');
	if (!thumbs)
		return false;

	if (reset)
		thumbs.innerHTML = '';
	thumbs.className = settings.thumbnailsSize.value;

	var img, link;
	for (var i = 0; i < imgs.length; i++) {
		img = document.createElement('img');
		img.src = imgs[i].thumb;

		link = document.createElement('a');
//		link.addEventListener('click', clickThumbnail);
		link.className = 'thumbnail';
		link.id = 'thumb_' + imgs[i].pos;
		link.rel = imgs[i].pos;
		link.href = imgs[i].full;
        link.target = '_blank';
		link.appendChild(img);

		thumbs.appendChild(link);
	}
}

/**
 * Generates the DOM objects needed for the scrolling mode
 * @param	Array	The objects of all images to be displayed
 * @param	Bool	(Optional) If the existing gallery images should be removed (default: false)
 */
function populateScrollingGallery(imgs, reset) {
	reset = (typeof reset === "undefined") ? false : reset;

	var gallery = document.getElementById('gallery');
	if (!gallery)
		return false;

	if (reset)
		gallery.innerHTML = '';

	var container, img, link, spinner;
	for (var i = 0; i < imgs.length; i++) {
		img = document.createElement('a');
		img.text = 'image '+imgs[i].pos;
		img.rel = imgs[i].pos;
		img.href = imgs[i].full;

		spinner = document.createElement('span');
		spinner.className = 'spinner';

		link = document.createElement('a');
		link.className = 'image';
		link.href = imgs[i].full;
		link.id = 'full_' + (((pagination.page - 1) * pagination.limit) + i);

		link.appendChild(img);
//		link.appendChild(spinner);

		container = document.createElement('p');
		container.appendChild(link);

		gallery.appendChild(container);
	}

	if (pagination.append && (imgs.length < totalImages)) {
		var loader = document.getElementById('loader');
		if (!loader) {
			loader = document.createElement('a');
			loader.id = 'loader'
			loader.innerHTML = 'Load Next Page of Images';
			loader.addEventListener('click', loadNextPage);
		}
		gallery.appendChild(loader);
	}
}

/**
 * Generates the DOM objects needed for the slideshow mode
 * @param	Object	The object representing the active image
 */
function buildSlideshowGallery(active) {
	var gallery = document.getElementById('gallery');
	if (!gallery)
		return false;

	gallery.innerHTML = '';

	var prev = document.createElement('a');
	prev.addEventListener('click', prevImage);
	prev.id = 'prev';
	prev.className = 'nav';
	prev.innerHTML = '<span class="arrow"><</span>';

	var next = document.createElement('a');
	next.addEventListener('click', nextImage);
	next.id = 'next';
	next.className = 'nav';
	next.innerHTML = '<span class="arrow">></span>';

	var img = document.createElement('img');
	img.alt = '';
	img.id = 'slideshowImage';
	img.src = active.full;

	var spinner = document.createElement('span');
	spinner.className = 'spinner';

	var link = document.createElement('a');
	link.className = 'image';
	link.href = active.full;
	link.id = 'slideshowLink';

	link.appendChild(img);
	link.appendChild(spinner);

	gallery.appendChild(link);
	gallery.appendChild(prev);
	gallery.appendChild(next);

	// resize the slideshow area
	resizeSlideshowGallery();
}

/**
 * Resizes the DOM object containing the slideshow image to accomodate non-fixed header and footer sizes
 */
function resizeSlideshowGallery() {
	var content = document.getElementById('content');
	if (!content)
		return false;

	// header
	var header = document.getElementById('header');
	content.style.top = (header) ? header.offsetHeight + 'px' : '';

	// footer
	var footer = document.getElementById('footer');
	content.style.bottom = (footer) ? footer.offsetHeight + 'px' : '';
}

/**
 * Generates the DOM objects for the header of rebuilt pages
 */
function buildHeader() {
	var header = document.getElementById('header');
	if (!header)
		return false;

	header.innerHTML = '';

	// logo
	var logo = document.createElement('a');
	logo.href = window.location.protocol + '//' + window.location.host;
	logo.id = 'logo';
	logo.innerHTML = '<span class="image">Image</span><span class="fap">Fap</span>';
	header.appendChild(logo);

	// heading
	var heading = document.createElement('h2');
	heading.id = 'heading';
	heading.innerHTML = '"<span class="title">' + stripslashes(title) + '</span>"';
	if (author.name && author.url)
		heading.innerHTML += ' <small>by</small> <a href="' + author.url + '">' + unescape(stripslashes(author.name)) + '</a>';
	header.appendChild(heading);

	// description
	if (desc) {
		var description = document.createElement('p');
		description.id = 'description';
		description.innerHTML = stripslashes(desc);
		header.appendChild(description);

		// if the description spans multiple lines, left-align the text
		if (description.offsetHeight > 16)
			description.style.textAlign = 'left';
	}

	// sub-heading
	var subheading = document.createElement('p');
	header.appendChild(subheading);
	// sub-heading > position
	var position = document.createElement('span');
	position.className = settings.mode.value;
	position.id = 'position';
	subheading.appendChild(position);
	// sub-heading > spacer
	var separator = document.createElement('span');
	separator.innerHTML = ' | ';
	subheading.appendChild(separator);
	// sub-heading > "toggle thumbnails" link
	var toggle = document.createElement('a');
	toggle.addEventListener('click', toggleThumbs);
	toggle.innerHTML = 'Toggle Thumbnails';
	subheading.appendChild(toggle);
	// sub-heading > spacer
	var separator = document.createElement('span');
	separator.innerHTML = ' | ';
	subheading.appendChild(separator);
	// sub-heading > "change settings" link
	var openSettings = document.createElement('a');
	openSettings.addEventListener('click', changeSettings);
	openSettings.innerHTML = 'Change Settings';
	subheading.appendChild(openSettings);

	// pagination
	if (settings.mode.value === 'scrolling' && pagination.active)
		buildPagination('header');

	// search form
	var form = document.createElement('form');
	form.id = 'search';
	form.method = 'POST';
	form.action = window.location.protocol + '//' + window.location.host + '/gallery.php';
	header.appendChild(form);
	// search form > text input
	var search = document.createElement('input');
	search.type = 'text';
	search.value = 'Enter search term(s)...';
	search.name = 'search';
	search.addEventListener('focus', function() { if (this.value === 'Enter search term(s)...') this.value = ''; });
	search.addEventListener('blur', function() { if (this.value === '') this.value = 'Enter search term(s)...'; });
	form.appendChild(search);
	// search form > submit button
	var submit = document.createElement('input');
	submit.type = 'submit';
	submit.value = 'Search';
	submit.name = 'submit';
	form.appendChild(submit);
}

/**
 * Generates the DOM objects for the footer of rebuilt pages
 */
function buildFooter() {
	var footer = document.getElementById('footer');
	if (!footer)
		return false;

	footer.innerHTML = '';

	footer.appendChild(addFavLink);

	if (settings.mode.value === 'scrolling' && pagination.active)
		buildPagination('footer');

	buildInfo();
	buildAutoplay();
}

/**
 * Updates the HTML for the position of the current image(s) within the gallery
 * @param	Int		(Optional) The lower limit image number (default: use existing value from DOM)
 * @param	Int		(Optional) The upper limit image number (default: use existing value from DOM)
 * @param	Int		(Optional) The total image number (default: use existing value from DOM)
 * @param	Int		(Optional) The active image number (default: use existing value from DOM)
 */
function updatePosition(lower, upper, total, active) {
	var position = document.getElementById('position');
	if (!position)
		return false;

	if (position.className !== settings.mode.value) {
		position.className = settings.mode.value;
		position.innerHTML = '';
	}

	if (settings.mode.value === 'scrolling') {
		if (position.innerHTML === undefined || position.innerHTML === '')
			position.innerHTML = 'Viewing image(s) <span id="position_lower">' + lower + '</span>-<span id="position_upper">' + upper + '</span> of <span id="position_total">' + total + '</span>';

		lower = (typeof lower === "undefined") ? parseInt(document.getElementById('position_lower').innerHTML, 10) : lower;
		upper = (typeof upper === "undefined") ? parseInt(document.getElementById('position_upper').innerHTML, 10) : upper;
		total = (typeof total === "undefined") ? parseInt(document.getElementById('position_total').innerHTML, 10) : total;
		if (upper > totalImages)
			upper = totalImages;
		document.getElementById('position_lower').innerHTML = lower;
		document.getElementById('position_upper').innerHTML = upper;
		document.getElementById('position_total').innerHTML = total;
	} else if (settings.mode.value === 'slideshow') {
		if (position.innerHTML === undefined || position.innerHTML === '')
			position.innerHTML = 'Viewing image <span id="position_active">' + lower + '</span> of <span id="position_total">' + total + '</span>';

		active = (typeof active === "undefined") ? parseInt(document.getElementById('position_active').innerHTML, 10) : active;
		total = (typeof total === "undefined") ? parseInt(document.getElementById('position_total').innerHTML, 10) : total;
		document.getElementById('position_active').innerHTML = active;
		document.getElementById('position_total').innerHTML = total;
	}
}

/**
 * Generates the DOM objects for the pagination of rebuilt gallery pages
 * @param	String	The ID of the DOM object of which to insert the pagination controls
 */
function buildPagination(location) {
	var container = document.getElementById(location);
	if (!container)
		return false;

	var pages = Math.ceil(totalImages / pagination.limit);

	if (pages <= 1)
		return false;

	var wrapper = document.createElement('div');
	wrapper.className = 'pagination';

	// previous page
	var prev = document.createElement('a');
	prev.innerHTML = '&laquo; Prev';
	if (pagination.page > 1) {
		prev.addEventListener('click', clickPagination);
		prev.rel = (pagination.page - 1);
	} else {
		prev.className = 'disabled';
	}
	wrapper.appendChild(prev);

	// individual pages
	var link, lower, upper;
	for (var i = 1; i <= pages; i++) {
		link = document.createElement('a');
		link.rel = i;

		lower = (pagination.limit * (i - 1) + 1);
		upper = (i < pages) ? (pagination.limit * i) : totalImages;
		link.innerHTML = (lower === upper) ? lower : lower + '-' + upper;

		if (i === pagination.page) {
			link.className = 'current';
		} else {
			link.addEventListener('click', clickPagination);
		}
		wrapper.appendChild(link);
	}

	// next page
	var next = document.createElement('a');
	next.innerHTML = 'Next &raquo;';
	if (pagination.page < pages) {
		next.addEventListener('click', clickPagination);
		next.rel = (pagination.page + 1);
	} else {
		next.className = 'disabled';
	}
	wrapper.appendChild(next);

	container.appendChild(wrapper);
}

/**
 * Updates the pagination controls
 * @param	Int		The number of the current page
 * @param	Bool	(Optional) If all pagination controls should be reset (default: false)
 */
function updatePagination(page, reset) {
	reset = (typeof reset === "undefined") ? false : reset;

	var containers = document.getElementsByClassName('pagination');
	if (containers.length === 0)
		return false;

	var links, rel;
	for (var i = 0; i < containers.length; i++) {
		links = containers[i].getElementsByTagName('a');

		// first handle all of the inner links (i.e. the numbered ones)
		if (reset) {
			// this is for when a page is loaded by itself
			// activate the target link and reset all of the other links to default
			for (var j = 1; j < (links.length - 1); j++) {
				if (j === page) {
					links[j].className = 'current';
					links[j].removeEventListener('click', clickPagination);
				} else {
					links[j].className = '';
					links[j].addEventListener('click', clickPagination);
				}
			}
		} else {
			// this is for when a page is appended
			// simply activate the target link
			links[page].className = 'current';
			links[page].removeEventListener('click', clickPagination);
		}

		// the first link is the "Prev" link, which needs to point to the page BEFORE the current page
		var prev = links[0];
		if (prev.nextSibling && prev.nextSibling.className === 'current') {
			prev.rel = page;
			prev.className = 'disabled';
			prev.removeEventListener('click', clickPagination);
		} else {
			prev.rel = (page - 1);
			prev.className = trim(links[0].className.replace('disabled', ''));
			prev.addEventListener('click', clickPagination);
		}

		// the last link is the "Next" link, which needs to point to the page AFTER the current page
		var next = links[(links.length - 1)];
		if (next.previousSibling && next.previousSibling.className === 'current') {
			next.rel = page;
			next.className = 'disabled';
			next.removeEventListener('click', clickPagination);
		} else {
			next.rel = (page + 1);
			next.className = trim(next.className.replace('disabled', ''));
			next.addEventListener('click', clickPagination);
		}
	}
}

/**
 * Generates the DOM objects for the information text at the bottom of the footer
 */
function buildInfo() {
	var footer = document.getElementById('footer');
	if (!footer)
		return false;

	var info = document.createElement('p');
	info.id = 'info';

	// "about" link
	var about = document.createElement('a');
	about.addEventListener('click', displayAbout);
	about.innerHTML = GM_info.script.name + ' v' + GM_info.script.version;
	info.appendChild(about);

	// spacer
	var separator = document.createElement('span');
	separator.innerHTML = ' | ';
	info.appendChild(separator);

	// "help" link
	var help = document.createElement('a');
	help.addEventListener('click', displayHelp);
	help.innerHTML = 'Help (?)';
	info.appendChild(help);

	// spacer
	var separator = document.createElement('span');
	separator.innerHTML = ' | ';
	info.appendChild(separator);

	// "settings" link
	var openSettings = document.createElement('a');
	openSettings.addEventListener('click', changeSettings);
	openSettings.innerHTML = 'Settings';
	info.appendChild(openSettings);

	footer.appendChild(info);
}

/**
 * Generates the DOM objects for the autoplay indicator in the footer
 */
function buildAutoplay() {
	var footer = document.getElementById('footer');
	if (!footer)
		return false;

	var autoplay = document.createElement('div');
	autoplay.id = 'autoplay';
	if (settings.mode.value === 'scrolling') {
		var disabled = document.createElement('span');
		disabled.innerHTML = 'Autoplay is only available in slideshow mode';
		autoplay.appendChild(disabled);
	} else if (settings.mode.value === 'slideshow') {
		// autoplay counter
		var counter = document.createElement('span');
		counter.id = 'counter';
		counter.innerHTML = (autoplay.count > 0) ? 'Advancing image in ' + autoplay.count + ' seconds' : 'Autoplay is disabled';
		autoplay.appendChild(counter);

		// spacer
		var separator = document.createElement('span');
		separator.innerHTML = ' | ';
		autoplay.appendChild(separator);

		// autplay control link
		var control = document.createElement('a');
		control.addEventListener('click', toggleAutoplay);
		control.id = 'control';
		control.innerHTML = 'Start Autoplay';
		autoplay.appendChild(control);
	}

	footer.appendChild(autoplay);
}

/**
 * Clears out and re-initializes the DOM with the basic HTML needed for the gallery
 */
function initDOM() {
	document.removeChild(document.getElementsByTagName('html')[0]);

	var html = document.createElement('html');
	head = document.createElement('head');
	body = document.createElement('body');
	html.appendChild(head);
	html.appendChild(body);
	document.appendChild(html);

	head.innerHTML = '<title>' + stripslashes(title) + '</title>';

	// build the basic HTML structure to prevent missing elements
	body.innerHTML = '<div id="header"></div><div id="content"><div id="gallery"></div></div><div id="footer"></div>'
}

/**
 * Registers the GreaseMonkey Menu commands
 * Must be run after the gallery page is initially built
 */
function initMenuCommands() {
	GM_registerMenuCommand('[' + GM_info.script.name + '] Help', displayHelp);
	GM_registerMenuCommand('[' + GM_info.script.name + '] About', displayAbout);
	GM_registerMenuCommand('[' + GM_info.script.name + '] Settings', changeSettings);
}

/**
 * Rebuilds the actual gallery page piece by piece
 */
function rebuildGalleryPage() {

	hideDialog();
	// re-initialize all settings and feature packages
	initSettings();
	initAutoplay();
	initPagination();
	initPreloading();

	// manually remove existing CSS and apply the chosen theme's CSS
	var styles = head.getElementsByTagName('style');
	for (var i = 0; i < styles.length; i++) {
		head.removeChild(styles[i]);
	}
	GM_addStyle(getCSS());

	// paginate the images using current pagination settings
	images = paginateImages(images);

	// header
	var header = document.getElementById('header');
	if (!header) {
		header = document.createElement('div');
		header.id = 'header';
		body.appendChild(header);
	}
	buildHeader();

	// thumbnails
	var thumbs = document.getElementById('thumbnails');
	if (!thumbs) {
		thumbs = document.createElement('div');
		thumbs.id = 'thumbnails';
		header.appendChild(thumbs);
	}

	// content (gallery wrapper)
	var content = document.getElementById('content');
	if (!content) {
		content = document.createElement('div');
		content.id = 'content';
		body.appendChild(content);
	}

	// main gallery
	var gallery = document.getElementById('gallery');
	if (!gallery) {
		gallery = document.createElement('div');
		gallery.id = 'gallery';
		content.appendChild(gallery);
	}

	// footer
	var footer = document.getElementById('footer');
	if (!footer) {
		footer = document.createElement('div');
		footer.id = 'footer';
		body.appendChild(footer);
	}
	buildFooter();

	// populate the thumbnails
	if (settings.thumbnails.value === false)
		thumbs.style.display = 'none';
	if (settings.mode.value === 'slideshow')
		thumbs.addEventListener('DOMMouseScroll', scrollThumbs, false);
	populateThumbnails(images[(pagination.page - 1)], true);

	// populate the gallery
    /*
	if (settings.mode.value === 'scrolling') {
		gallery.style.marginBottom = (footer.offsetHeight + 10) + 'px';
		body.className = 'scrolling';
		var lower = ((pagination.limit * (pagination.page-1)) + 1);
		var upper = (pagination.limit * pagination.page);
		updatePosition(lower, upper, totalImages, undefined);
		populateScrollingGallery(images[(pagination.page - 1)], true);
	} else if (settings.mode.value === 'slideshow') {
		body.className = 'slideshow';
		updatePosition(undefined, undefined, totalImages, activeImage);
		buildSlideshowGallery(images[(pagination.page - 1)][activeImage]);
		refreshNavigation();
	}
    */

	// remove the page hash for slideshow mode
	if (settings.mode.value === 'slideshow')
		unsetHashParam('page');

	// show the active image, unless it is the first image of a scrolling gallery
	if ((settings.mode.value === 'slideshow') || (activeImage > 0))
		showImage();

	// start preloading images if preloading is enabled
	if ((settings.mode.value === 'slideshow') && preloading.active)
		preloadImage();
}

/**
 * main function; executes functionality based on page (via URL)
 * Redirects to the one-page version of galleries (if not already in one-page mode)
 */
function init() {
	// prevent the initialization function from running multiple times
	if (loaded) {
		return false;
	} else {
		loaded = true;
	}

	var loc = window.location.href;
    if (loc.indexOf('/photo/') > 0) {
        window.location.assign($(".advance-link img")[0].src);
        return;
    }
	if ((loc.indexOf('/gallery/') !== -1) || (loc.indexOf('/pictures/') !== -1)) {
		// this is a gallery page; make sure it is in "One Page" mode and then go to work
		if (loc.indexOf('view') === -1) {
			window.location.href += ((loc.indexOf('?') === -1) ? '?' : '&') + 'view=2';
		} else {
			// populate the global variables from the original gallery page
			title = document.title;
			if (title)
				title = trim(title.replace('Porn pics of ', '').replace(' (Page 1)', ''));

			var links = document.getElementsByTagName('a');
			if (links) {
				var i = 0;
				for (var i = 0; i < links.length; i++) {
					if (links[i].href.indexOf('profile.php?') !== -1) {
						author.name = links[i].href.split('=')[1];
						author.url	= links[i].href;
						break;
					}
				}
			}

			desc = document.getElementById('cnt_description');
			if (desc)
				desc = trim(desc.textContent);

			addFavLink = document.getElementById('favorites_container');

			// FINALLY! grab the images, build the gallery, enable the hotkeys, and listen for changes to settings
			images = findImages();
			initDOM();
			rebuildGalleryPage();
			initMenuCommands();
			checkSettings();	// must run after the gallery page is built (otherwise the notification gets wiped out)
			document.addEventListener('keydown', onKeyDown, false);
			window.addEventListener('focus', onWindowFocus, false);
			window.addEventListener('scroll', onWindowScroll, false);
			window.addEventListener('mozfullscreenchange', onFullScreenChange, false);
			window.addEventListener('webkitfullscreenchange', onFullScreenChange, false);
		}
	} else {
		// this might be a page with links to galleries; change all of the links to galleries to "One Page" mode
		var links = document.getElementsByTagName('a');
		if (links) {
			for (var i = 0; i < links.length; i++) {
				if ((links[i].href.indexOf('gallery') !== -1) || (links[i].href.indexOf('pictures') !== -1)) {
					links[i].href += ((links[i].href.indexOf('?') === -1) ? '?' : '&') + 'view=2';
				}
			}
		}
	}
}

/**
 * Toggles visibility of the navigational arrows by adding/removing a "disabled" class
 */
function refreshNavigation() {
	var nav = document.getElementsByClassName('nav');
	for (var i = 0; i < nav.length; i++) {
		nav[i].className = trim(nav[i].className.replace('disabled',''));
	}

	if (activeImage === 0) {
		var prev = document.getElementById('prev');
		if (prev)
			prev.className += ' disabled';
	} else if (activeImage === (pagination.limit - 1)) {
		var next = document.getElementById('next');
		if (next)
			next.className += ' disabled';
	}
}

/**
 * Handles the click event for the pagination links
 * @param	Event	The click event
 */
function clickPagination(evt) {
	if (this.rel) {
		var page = parseInt(this.rel, 10);
		activeImage = (((page - 1) * pagination.limit) + 1);
		unsetHashParam('image');
		updatePagination(page, true);
		goToPage(page);
	}
}

/**
 * Sets the values needed to change pages and rebuilds the gallery for the specified page
 * @param	Int		The number of the page to display
 */
function goToPage(page) {
	pagination.page = page;

	var lower = ((pagination.limit * (pagination.page-1)) + 1);
	var upper = (pagination.limit * pagination.page);

	setHashParam('page', page);
	setHashParam('image', lower);

	updatePosition(lower, upper, totalImages, undefined);

	//populateScrollingGallery(images[(page - 1)], true);
	populateThumbnails(images[(page - 1)], true);

	window.scrollTo(0, 0);
}

/**
 * Captures mouse scrolling on the thumbnails and scrolls the images horizontally
 * @param	Event	mouse wheel scroll event
 */
function scrollThumbs(evt){
	if (!evt)
		evt = this;
	evt.preventDefault();	// prevent vertical scrolling on the page

	var delta = (evt.detail) ? evt.detail : 0;
	window.document.getElementById('thumbnails').scrollLeft += (delta * 10);

	evt.returnValue = false;
}

/**
 * Sets the active image based on the thumbnail image that was clicked
 * Expects to be executed as a click event for a DOM object with a "rel" value
 * @param	Event	The click event
 */
function clickThumbnail(evt) {
	if (this.rel)
		activeImage = parseInt(this.rel, 10);
	showImage();
}

/**
 * Sets the active image to the next image
 * Ensures the active image is not the last one already
 */
function nextImage() {
	if (settings.mode.value === 'slideshow' && autoplay.active) {
		autoplay.count = autoplay.delay;
	}

	if (activeImage < ((pagination.page * pagination.limit) - 1)) {
		activeImage++;
		showImage();
	}
}

/**
 * Sets the active image to the previous image
 * Ensures the active image is not the first one already
 */
function prevImage() {
	if (settings.mode.value === 'slideshow' && autoplay.active) {
		stopAutoplay();
	}

	if (activeImage > ((pagination.page - 1) * pagination.limit)) {
		activeImage--;
		showImage();
	}
}

/**
 * Sets the internal activeImage pointer and sets the image hash parameter
 * Activates the correct thumbnail for the active image
 * @param	Int		(Optional) The number of the active image (default: value of activeImage internal variable)
 */
function setActiveImage(pos) {
	pos = (typeof pos === "undefined") ? activeImage : pos;

	setHashParam('page', findImagePage());
	setHashParam('image', (pos + 1));
	showActiveThumb(pos);
}

/**
 * Highlights the thumbnail corresponding to the current image
 * and scrolls it into view in slideshow mode
 * @param	Int		(Optional) The number of the active image (default: value of activeImage internal variable)
 */
function showActiveThumb(pos) {
	pos = (typeof pos === "undefined") ? activeImage : pos;

	var thumbs = document.getElementsByClassName('thumbnail');
	for (var i = 0; i < thumbs.length; i++) {
		thumbs[i].className = trim(thumbs[i].className.replace('active',''));
	}

	var thumb = document.getElementById('thumb_' + pos);
	if (thumb) {
		thumb.className += ' active';
		if (settings.mode.value === 'slideshow')
			thumb.scrollIntoView();
	}
}

/**
 * Displays/navigates to the "active" image
 * In scrolling mode, this simply scrolls the page up/down to the active image,
 * but in slideshow mode, this shows the active image and hides the others
 * @param	Int		(Optional) The number of the active image (default: value of activeImage internal variable)
 */
function showImage(pos) {
	pos = (typeof pos === "undefined") ? activeImage : pos;

	setActiveImage(pos);

	if (settings.mode.value === 'scrolling') {
		var target = document.getElementById('full_' + pos);
		if (target)
			target.scrollIntoView();
	} else if (settings.mode.value === 'slideshow') {
		var link = document.getElementById('slideshowLink');
		var img	 = document.getElementById('slideshowImage');

		if (link && img) {
			showNotification("Loading image " + (pos + 1), 1000);
			// first blank out the current image and link target from the previous image
			// then use a slight delay to set the next image and link target;
			// this causes the loading animation to be played while the image is loaded;
			// if the delay is removed, the previous image will remain visible until the new image is loaded
			// without any indication that the image has changed and the next image is loading
			img.src = link.href = '';
			setTimeout(function() { img.src = link.href = images[pagination.page-1][pos].full; }, 500);

			// always remove the autoplay function from load, and then re-add again if autoplay is still enabled
			img.removeEventListener('load', startAutoplay);
			if (autoplay.active && !autoplay.paused) {
				img.addEventListener('load', startAutoplay);
			}

			updatePosition(undefined, undefined, totalImages, (pos + 1));
			refreshNavigation();

			if (preloading.active && !preloading.done && (preloading.pos < activeImage))
				preloading.pos = activeImage;
		}
	}
}

/**
 * Event handler for keypresses
 * Used to handle hotkeys
 * @param	Event	The keypress event
 */
function onKeyDown(evt) {
	if (!evt)
		evt = this;

	if ((evt.target.nodeName === 'INPUT') || (evt.target.nodeName === 'SELECT') || (evt.target.nodeName === 'TEXTAREA'))
		return false;

	var correct = true,
		hotkey;
	for (var h in hotkeys) {
		hotkey = hotkeys[h];
		for (var c in hotkey.codes) {
			if (evt.keyCode === hotkey.codes[c]) {
				for (var m in hotkey.modif) {
					correct = (evt[m] === hotkey.modif[m]) ? correct : false;
				}
				if (correct) {
					evt.preventDefault();
					return (typeof hotkey.action === 'function') ? hotkey.action.call() : eval(hotkey.action);
				}
			}
		}
	}
}

/**
 * Event handler for window scroll
 * Used to update the active image when manually scrolling in the scrolling gallery
 * and to determine if the next page of images should be loaded
 * @param	Event	The scroll event
 */
function onWindowScroll(evt) {
	if (settings.mode.value === 'scrolling') {
		var imgs = document.getElementById('gallery').getElementsByTagName('img');
		var target = 0;

		// loop backwards through the images until the currently visible image is found
		for (var i = (imgs.length - 1); i >= 0; i--) {
			var current = imgs[i].parentNode.offsetTop;

			if (document.body.scrollTop >= current) {
				target = parseInt(imgs[i].rel, 10);
				break;
			}
		}

		// only update the active image if it changed
		if (target !== activeImage) {
			activeImage = target;
			setActiveImage();
		}

		if (pagination.append) {
			var last = imgs[(imgs.length - 1)];
			if ((last.parentNode.offsetTop - window.innerHeight) <= window.pageYOffset) {
				loadNextPage();
			}
		}
	}
}

/**
 * Event handler for window focus
 * Used to monitor setting changes and refresh the gallery when needed
 * @param	Event	The focus event
 */
function onWindowFocus(evt) {
	var prevMode = settings.mode.value;
	var prevPagination = settings.pagination.value;
	initSettings();

	var refresh = false;

	var thumbs = document.getElementById('thumbnails');
	if (thumbs) {
		// thumbnails can be toggled without rebuilding the entire gallery page
		thumbs.className = settings.thumbnailsSize.value;
		thumbs.style.display = (settings.thumbnails.value === true) ? 'block' : 'none';
		if (settings.mode.value === 'slideshow')
			resizeSlideshowGallery();
	}

	if (prevMode !== settings.mode.value) {
		// current gallery mode does not match the stored preferences
		refresh = true;
	}

	if (settings.mode.value === 'scrolling') {
		if (prevPagination !== settings.pagination.value) {
			// current gallery pagination does not match the stored preferences
			refresh = true;
		}
	}

	if (refresh)
		rebuildGalleryPage();
}

/**
 * Displays the about dialog
 */
function displayAbout() {
	showDialog(getAbout(), 'About');
}

/**
 * Displays the help dialog
 */
function displayHelp() {
	showDialog(getHelp(), 'Help');
}

/**
 * Re-initialize the autoplay settings
 */
function initAutoplay() {
	if (autoplay.timer)
		window.clearTimeout(autoplay.timer);

	initSetting('autoplay');

	// reset autoplay object properties to default
	autoplay = {
		active: false,
		count:	parseInt(settings.autoplay.value, 10),
		delay:	parseInt(settings.autoplay.value, 10),
		paused: false,
		timer:	undefined
	};
}

/**
 * Starts the autoplay used timer for advancing to the next image
 * If the delay is not set correctly, the user is prompted to set it
 */
function startAutoplay() {
	if (!autoplay.active)
		return false;

	if (autoplay.delay > 1000) {
		// the delay is likely in milliseconds, so convert it to seconds and save
		autoplay.delay /= 1000;
		GM_setValue(settings.autoplay.name, autoplay.delay);
		initAutoplay();
	}

	if (autoplay.delay < 0) {
		// the delay is invalid; inform the user
		var buttons = {0: {text: 'Change Settings', action: changeSettings}};
		showDialog('<p>The current value for the ' + settings.autoplay.label + ' is not valid.</p>', 'Invalid ' + settings.autoplay.label, buttons);
	} else {
		autoplay.count = autoplay.delay;
		if (activeImage < totalImages) {
			autoplayTimer();
		} else {
			stopAutoplay();
		}
	}
}

/**
 * Stops the autoplay timer used for advancing to the next image
 */
function stopAutoplay() {
	window.clearTimeout(autoplay.timer);
	autoplay.active = false;
	autoplay.count = autoplay.delay;

	var counter = document.getElementById('counter');
	if (counter)
		counter.innerHTML = 'Autoplay is disabled';

	var control = document.getElementById('control');
	if (control)
		control.innerHTML = 'Start Autoplay';
}

/**
 * Pauses the autoplay timer
 */
function pauseAutoplay() {
	window.clearTimeout(autoplay.timer);
	autoplay.paused = true;

	var counter = document.getElementById('counter');
	if (counter)
		counter.innerHTML = 'Autoplay is paused';

	var control = document.getElementById('control');
	if (control)
		control.innerHTML = 'Resume Autoplay';
}

/**
 * Resumes the autoplay timer
 */
function resumeAutoplay() {
	var counter = document.getElementById('counter');
	if (counter)
		counter.innerHTML = 'Resuming autoplay...';

	var control = document.getElementById('control');
	if (control)
		control.innerHTML = 'Pause Autoplay';

	autoplay.paused = false;
	autoplayTimer();
}

/**
 * Starts the counter indicator for advancing to the next image
 */
function autoplayTimer() {
	window.clearTimeout(autoplay.timer);

	var counter = document.getElementById('counter');
	if (!counter)
		return false;

	if (activeImage < (totalImages - 1)) {
		if (autoplay.count > 0) {
			autoplay.timer = window.setTimeout(autoplayTimer, 1000);
			counter.innerHTML = 'Advancing image in <b>' + autoplay.count + '</b> seconds';
		} else {
			counter.innerHTML = 'Loading image...';
			nextImage();
		}
		autoplay.count--;
	} else {
		counter.innerHTML = 'End of gallery';
	}
}

/**
 * Toggles autoplay (start and pause/resume)
 */
function toggleAutoplay() {
	autoplay.active = !autoplay.active;
	if (autoplay.active) {
		resumeAutoplay();
	} else {
		pauseAutoplay();
	}

	if (fullscreen)
		showAutoplayIndicator();
}

/**
 * Displays an indicator when toggling autoplay
 */
function showAutoplayIndicator() {
	var gallery = document.getElementById('gallery');
	if (!gallery)
		return false;

	var indicator = document.getElementById('indicator');
	if (indicator)
		hideAutoplayIndicator(0);

	indicator = document.createElement('div');
	indicator.id = 'indicator';
	indicator.style.opacity = 1;
	gallery.appendChild(indicator);

	if (autoplay.active) {
		indicator.innerHTML = '<span class="symbol play">&#9658;</span>';
	} else {
		indicator.innerHTML = '<span class="symbol pause">||</span>';
	}
	setTimeout(function() { hideAutoplayIndicator(100); }, 1000);
}

/**
 * Fades the autoplay indicator
 */
function hideAutoplayIndicator(duration) {
	duration = (typeof duration === "undefined") ? 100 : duration;

	var indicator = document.getElementById('indicator');
	if (!indicator)
		return false;

	var timer = setInterval(function() {
		if (indicator === null || indicator.parentNode === null) {
			clearInterval(timer);
		} else {
			indicator.style.opacity -= 0.1
			if (indicator.style.opacity <= 0)
				indicator.parentNode.removeChild(indicator);
		}
	}, duration);
}

/**
 * Toggles the slideshow mode for gallery pages
 * A dialog is shown after toggling to allow the user to apply the change immediately
 * @param	Bool	(Optional) If a nofitication should be displayed (default: true)
 */
function switchGalleryMode(notify) {
	notify = (typeof notify === "undefined") ? true : notify;

	if (settings.mode.value === 'slideshow')
		GM_setValue(settings.mode.name, 'scrolling');
	else if (settings.mode.value === 'scrolling')
		GM_setValue(settings.mode.name, 'slideshow');
	initSetting('mode');

	if (notify) {
		var buttons = {0: {text: 'Apply Change Now', action: rebuildGalleryPage}, 1: {text: 'Close', action: hideDialog}};
		showDialog('<p>Gallery mode has been changed to ' + settings.mode.value + '.</p>', 'Gallery Mode', buttons);
	}
}

/**
 * Toggles visibilty on thumbnails and stores the preference
 */
function toggleThumbs() {
	GM_setValue(settings.thumbnails.name, !GM_getValue(settings.thumbnails.name, settings.thumbnails.def));
	initSetting('thumbnails');

	var visible = settings.thumbnails.value;

	var thumbs = document.getElementById('thumbnails');
	if (thumbs) {
		var thumbsHeight = thumbs.offsetHeight;
		thumbs.style.display = (visible) ? 'block' : 'none';
		showNotification('Thumbnails are now ' + ((visible) ? 'visible' : 'hidden'), 1000);

		if (settings.mode.value === 'slideshow') {
			resizeSlideshowGallery();
		} else if (settings.mode.value === 'scrolling') {
			if (visible) {
				// scroll the window up to the top of the page when thumbnails are visible
				window.scrollTo(0, 0);
			} else {
				// attempt to prevent the page from scrolling too much when thumbnails are hidden
				window.scrollTo(0, (window.pageYOffset - thumbsHeight - 10));
			}
		}
	}
}

/**
 * Re-initialize the preloading settings
 */
function initPreloading() {
	initSetting('preloading');

	// reset autoplay object properties to default
	var preloading = {
		active: settings.preloading.value,
		done:	false,
		pos:	activeImage
	};
}

/**
 * Preloads the next image in slideshow mode
 * Will be called continuously until the last image is loaded
 */
function preloadImage() {
	if (!preloading.active)
		return false;

	if (preloading.pos < (totalImages - 1)) {
		preloading.pos++;
		showNotification("Preloading image " + (preloading.pos + 1), 1000);
		var preloader = new Image();
		preloader.src = images[pagination.page-1][preloading.pos].full;
		preloader.addEventListener('load', preloadImage);
	} else {
		preloading.done = true;
	}
}

/**
 * Toggles full screen view in supported browsers
 * Full screen view is only available in slideshow mode
 */
function toggleFullScreen() {
	if (settings.mode.value !== 'slideshow')
		return false;

	if (!document.mozFullScreenElement && !document.webkitFullscreenElement) {
		var target = document.getElementById('content');
		if (target.mozRequestFullScreen) {
			target.mozRequestFullScreen();
		} else if (target.webkitRequestFullscreen) {
			target.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
		} else {
			showDialog('Sorry, but it seems your browser does not support customized fullscreen HTML.', 'Fullscreen Error');
		}
	} else {
		if (document.mozCancelFullScreen) {
			document.mozCancelFullScreen();
		} else if (document.webkitCancelFullScreen) {
			document.webkitCancelFullScreen();
		}
	}
}

/**
 * Event handler for fullscreen enter/exit
 * Used to move the thumbnails container into the c#ontent container in fullscreen mode
 * and back to the #header container when not in fullscreen mode
 * @param   Event   fullscreenchange event
 */
function onFullScreenChange(evt) {
	fullscreen = !(document.mozFullScreenElement === null || document.webkitFullscreenElement === null);
	if (fullscreen) {
		document.getElementById('content').insertBefore(document.getElementById('thumbnails'), document.getElementById('gallery'));
	} else {
		document.getElementById('header').appendChild(document.getElementById('thumbnails'));
	}
	resizeSlideshowGallery();
}



/*	===== Dialog Functions =====
	Rather than using boring alert() and prompt() boxes, which are finicky in Greasemonkey,
	this script creates a div, styled with CSS, to replicate that functionality.
*/

/**
 * Generates the DOM elements for the dialog boxes
 */
function buildDialog() {
	var dialog = document.getElementById('dialogBox');
	if (dialog) {
		resetDialog();
		return;
	}

	dialog = document.createElement('div');
	dialog.id = 'dialogBox';
	dialog.innerHTML = '<h3 id="dialogTitle"></h3><div id="dialogMessage"></div><div id="dialogButtons"></div>';

	var closeButton = document.createElement('a');
	closeButton.addEventListener('click', hideDialog, false);
	closeButton.id = 'dialogClose';
	closeButton.innerHTML = '&#10006;';
	dialog.appendChild(closeButton);

	var dialogContainer = document.createElement('div');
	dialogContainer.addEventListener('click', function(e) { hideDialog(e, true); }, false);
	dialogContainer.id = 'dialogContainer';
	dialogContainer.style.display = 'none';
	dialogContainer.appendChild(dialog);
	body.appendChild(dialogContainer);
}

/**
 * Hides the dialog box
 * @param	Event	The click event
 * @param	Bool	(Optional) If true, will only hide the dialog if the target of the click event is the dialogContainer
 */
function hideDialog(e, containerOnly) {
	containerOnly = (typeof containerOnly === "undefined") ? false : containerOnly;
	if (containerOnly && (typeof e !== "undefined") && (e.target.id !== 'dialogContainer'))
		return false;

	var dialogContainer = document.getElementById('dialogContainer');
	if (dialogContainer)
		dialogContainer.style.display = 'none';
}

/**
 * Vertically centers the dialog box on the screen
 */
function positionDialog() {
	var dialogBox		 = document.getElementById('dialogBox');
	var dialogContainer	 = document.getElementById('dialogContainer');

	dialogContainer.style.display = 'block';
	var top = ((window.innerHeight / 2) - (dialogBox.offsetHeight / 2) - 20);
	dialogBox.style.top = (top < 0) ? '0' : top + 'px';

	dialogContainer.scrollTop = 0;
}

/**
 * Clears out the contents of the dialog box
 */
function resetDialog() {
	document.getElementById('dialogTitle').innerHTML = '';
	document.getElementById('dialogMessage').innerHTML = '';
	document.getElementById('dialogButtons').innerHTML = '';
}

/**
 * Preloads the next image in slideshow mode
 * Will be called continuously until the last image is loaded
 * @param	String	The HTML content of the dialog box
 * @param	String	(Optional) A title for the dialog box (always prefixed with '[Script Name]')
 * @param	Object	(Optional) An object representing the buttons to display and their actions (default: close button)
 */
function showDialog(content, title, buttons) {
	buttons = (typeof buttons === "undefined") ? {0: {text: 'Close', action: hideDialog}} : buttons;
	title = (typeof title === "undefined") ? '' : title;

	buildDialog();

	var dialogContainer = document.getElementById('dialogContainer');
	var titleContainer = document.getElementById('dialogTitle');
	var messageContainer = document.getElementById('dialogMessage');
	var buttonContainer = document.getElementById('dialogButtons');

	titleContainer.innerHTML = '[<b>' + GM_info.script.name + '</b>] ' + title;
	messageContainer.innerHTML = content;

	var btn;
	for (var button in buttons) {
		btn = document.createElement('button');
		btn.addEventListener('click', buttons[button].action, false);
		btn.innerHTML = buttons[button].text;
		if(button == 0)
			btn.className = 'default';
		buttonContainer.appendChild(btn);
	}

	positionDialog();

	buttonContainer.childNodes[0].focus();
}



/*	===== Notication Message Functions =====
	The are notification messages that display in the lower right corner and are
	automatically removed after a short duration without needing user interaction.
*/

/**
 * Generates and displays a notification message
 * @param	String	The notification message text
 * @param	Int		(Optional) The delay (in milliseconds) before the message should be hidden (default: 5000)
 * @param	Int		(Optional) The duration (in milliseconds) over which the message should fade (default: 100)
 * @return	Object	The DOM object of the created notification
 */
function showNotification(text, delay, duration) {
	delay = (typeof delay === "undefined") ? 5000 : delay;

	var notificationContainer = document.getElementById('notificationContainer');
	if (!notificationContainer) {
		notificationContainer = document.createElement('div');
		notificationContainer.id = 'notificationContainer';
		document.getElementById('content').appendChild(notificationContainer);
	}
	notificationContainer.style.bottom = (document.getElementById('footer').offsetHeight + 10) + 'px';

	var notifications = notificationContainer.getElementsByClassName('notification');

	var notification = document.createElement('div');
	notification.className = 'notification';
	notification.id = 'notification' + notifications.length;
	notification.innerHTML = text;

	if (notifications.length > 0) {
		notificationContainer.insertBefore(notification, notifications[0]);
	} else {
		notificationContainer.appendChild(notification);
	}

	if (delay > 0)
		setTimeout(function() { hideNotification(notification, duration); }, delay);

	return notification;
}

/**
 * Fades a notification message out and then removes it
 * @param	Object	The notification message to hide
 * @param	Int		(Optional) The duration (in milliseconds) over which the message should fade (default: 100)
 */
function hideNotification(notification, duration) {
	duration = (typeof duration === "undefined") ? 100 : duration;

	if (!notification)
		return false;

	notification.style.opacity = 1;
	var timer = setInterval(function() {
		notification.style.opacity -= 0.1
		if (notification.style.opacity <= 0) {
			notification.parentNode.removeChild(notification);
			clearInterval(timer);
		}
	}, duration);
}


/*	===== Setting Functions =====
	Functions used for working with the individual settings in bulk
*/

/**
 * Saves the values for all settings from the Settings Form Dialog Box
 * Verifies values are valid based on criteria of the individual settings (min, max, etc.)
 */
function applySettings() {
	var setting, field, value;
	for (s in settings) {
		setting = settings[s];
		if (setting && (typeof setting === 'object')) {
			value = null;
			field = document.getElementById('setting-' + s);
			if (field) {
				switch (setting.type) {
					case 'integer':
						value = parseInt(field.value, 10);
						if (isNaN(value)) {
							var buttons = {0: {text: 'Try Again', action: changeSettings}};
							showDialog('<p>A numeric value must be provided for <b>' + setting.label + '</b>.</p>', 'Error', buttons);
							return false;
						}
						if ((typeof setting.min !== 'undefined') && (setting.min !== null) && (value < setting.min)) {
							var buttons = {0: {text: 'Try Again', action: changeSettings}};
							showDialog('<p>The value provided (<span class="error">' + value + '</span>) for <b>' + setting.label + '</b> must be greater than or equal to <b>' + setting.min + '</b>.</p>', 'Error', buttons);
							return false;
						}
						if ((typeof setting.max !== 'undefined') && (setting.max !== null) && (value > setting.max)) {
							var buttons = {0: {text: 'Try Again', action: changeSettings}};
							showDialog('<p>The value provided for (<span class="error">' + value + '</span>) <b>' + setting.label + '</b> must be less than or equal to <b>' + setting.max + '</b>.</p>', 'Error', buttons);
							return false;
						}
						break;
					case 'select':
						value = field.options[field.selectedIndex].value;
						break;
					case 'boolean':
						value = field.checked;
						break;
					default:
						value = field.value;
						break;
				}
			}
			if (value !== null) {
				settings[s].value = value;
			}
		}
	}
	saveSettings();
	var buttons = {0: {text: 'Apply Changes Now', action: rebuildGalleryPage}, 1: {text: 'Close', action: hideDialog}};
	showDialog('<p>Your changes have been saved successfully!</p>', 'Success', buttons);
}

/**
 * Checks if settings have been saved for the current script version.
 * For new installs, a notice is displayed forcing the user to save the settings for the first time;
 * for updates, the user can view the settings or dismiss the notice.
 */
function checkSettings() {
	var lastSavedVersion = GM_getValue('lastSavedVersion', false);
	if (!lastSavedVersion) {
		var buttons = {0: {text: 'Continue', action: changeSettings}};
		showDialog('<p>Since this is your first time using ' + GM_info.script.name + ', you need to view (and modify) the settings to meet your needs.</p><p>Note that all settings have a default value set already for your convenience; you can simply click the "Save Settings" button on the next dialog box to continue.', 'First Run', buttons);
	} else if (lastSavedVersion !== GM_info.script.version) {
		GM_setValue('lastSavedVersion', GM_info.script.version);
		var buttons = {0: {text: 'Change Settings', action: changeSettings}, 1: {text: 'Close', action: hideDialog}};
		showDialog('<p>The version of this script has changed from ' + lastSavedVersion + ' to ' + GM_info.script.version + '. There may be new settings for you to utilize.', 'Version Change', buttons);
	}
}

/**
 * Initializes all settings from saved preferences
 * Uses the default values for settings that have not yet been saved
 */
function initSettings() {
	for (s in settings) {
		initSetting(s)
	}
}

/**
 * Initializes the specified setting from saved preferences
 * Uses the default value for settings that have not yet been saved
 * @param	String	name (key) of the setting to initialize
 */
function initSetting(s) {
	if (settings[s] && (typeof settings[s] === 'object')) {
		if (settings[s].name)
			settings[s].value = GM_getValue(settings[s].name, settings[s].def);

		if (settings[s].type === 'integer')
			settings[s].value = parseInt(settings[s].value, 10);
		else if ((settings[s].type === 'select') && (settings[s].opts.indexOf(settings[s].value) === -1))
			settings[s].value = settings[s].def;
	}
}

/**
 * Generates the HTML for the Settings Form that will be displayed via dialog box
 */
function changeSettings() {
	initSettings();
	var setting;
	var html = '<form id="settingsForm">';
	for (s in settings) {
		setting = settings[s];
		if (setting && (typeof setting === 'object')) {
			html += '<fieldset><legend>' + setting.label + '</legend>';
			switch (setting.type) {
				case 'integer':
				case 'text':
					html += '<label for="setting-' + s + '">Enter a value for the ' + setting.label;
					if (setting.hint)
						html += ' (' + setting.hint + ')';
					html += ':<br/><span class="default">Default value: <b>' + setting.def + '</b></span></label>';
					html += '<input type="text" id="setting-' + s + '" name="' + s + '" value="' + setting.value + '" size="' + setting.size + '" maxlength="' + setting.size + '"/>';
					break;
				case 'select':
					html += '<label for="setting-' + s + '">Select a value for the ' + setting.label;
					if (setting.hint)
						html += ' (' + setting.hint + ')';
					html += ':<br/><span class="default">Default value: <b>' + capitalize(setting.def) + '</b></span></label>';
					html += '<select id="setting-' + s + '" name="' + s + '">';
					for (opt in setting.opts) {
						html += '<option value="' + setting.opts[opt] + '"' + ((setting.value === setting.opts[opt]) ? ' selected="selected"' : '') + '>' + capitalize(setting.opts[opt]) + '</option>';
					}
					html += '</select>';
					break;
				case 'boolean':
					html += '<input type="checkbox" id="setting-' + s + '" name="' + s + '" value="true"' + ((setting.value) ? ' checked="checked"' : '') + '/>';
					html += '<label for="setting-' + s + '">Enable ' + setting.label;
					if (setting.hint)
						html += ' (' + setting.hint + ')';
					html += '<br/><span class="default">Default value: <b>' + ((setting.def) ? 'Enabled' : 'Disabled') + '</b></span></label>';
					break;
			}
			html += '</fieldset>';
		}
	}
	showDialog(html, 'Settings', {0: {text: 'Save Settings', action: applySettings}, 1: {text: 'Cancel', action: hideDialog}});
}

/**
 * Saves the current value for each setting to local storage
 */
function saveSettings() {
	var setting;
	for (s in settings) {
		setting = settings[s];
		if (setting && (typeof setting === 'object'))
			GM_setValue(setting.name, setting.value);
	}
	initSettings();
	GM_setValue('lastSavedVersion', GM_info.script.version);
}



/*	===== String Functions =====
	Utility functions for manipulating strings
*/

/**
 * Escapes quotes and double-quotes in a string
 * @param	String	The string to escape
 * @return	String	The escaped string
 */
function addslashes(str) {
	return str.replace(/\\/g,'\\\\').replace(/\'/g,'\\\'').replace(/\"/g,'\\"').replace(/\0/g,'\\0');
}

/**
 * Unescapes quotes and double-quotes in a string
 * @param	String	The escaped string
 * @return	String	The string to escape
 */
function stripslashes(str) {
	return str.replace(/\\'/g,'\'').replace(/\\"/g,'"').replace(/\\0/g,'\0').replace(/\\\\/g,'\\');
}

/**
 * Removes leading and trailing whitepsace from a string
 * @param	String	The string to trim
 * @return	String	The trimmed string
 */
function trim(str) {
	return str.replace(/^\s+|\s+$/g,'');
}

/**
 * Capitalizes the first character of a string
 * @param	String	The string to capitalize
 * @return	String	The capitalized string
 */
function capitalize(str) {
	return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
}



/*	===== Hash Parameter Functions =====
	Utility functions for setting and retrieving data from the URL location hash
*/

/**
 * Returns all key/value pairs stored in the location hash
 * @return	Object	The location hash parameters indexed by key name
 */
function getHashParams() {
	var params = window.location.hash.replace('#!','').split('/');
	var ret = [];
	for (var i = 0; i < params.length; i = i + 2) {
		if (params[(i + 1)] && params[(i + 2)])
			ret[params[(i + 1)]] = params[(i + 2)];
	}
	return ret;
}

/**
 * Returns the value of the supplied hash key
 * @param	String	The name of the hash key
 * @return	String	The value of the hash key
 */
function getHashParam(key) {
	var params = getHashParams();
	return params[key] || undefined;
}

/**
 * Sets the value of the supplied hash key
 * @param	String	The name of the hash key
 * @param	String	The value of the hash key
 */
function setHashParam(key, val) {
	var hashString = '#!';
	var params = getHashParams();
	params[key] = val;
	for (key in params) {
		hashString += '/' + key + '/' + params[key];
	}
	history.replaceState(null, null, hashString);
}

/**
 * Removes a hash key/value pair from the location hash
 * @param	String	The name of the hash key
 */
function unsetHashParam(key) {
	var current = getHashParam(key);
	if (typeof current !== 'undefined')
		history.replaceState(null, null, window.location.hash.replace('/' + key + '/' + current, ''));
}



/*	===== Gallery CSS =====
	The CSS for the rebuilt gallery page with theme support
*/

/**
 * Generates the CSS used for the rebuilt gallery page
 * @param	String	(Optional) Additional CSS
 * @return	String	The CSS for the rebuilt gallery page
 */
function getCSS(css) {
	css = (typeof css === "undefined") ? '' : css;

	initSettings('theme');
	switch(getHashParam('theme') || settings.theme.value) {
		case 'blue':
			var bg1 	= '#060D1A';
			var bg2 	= '#03060D';
			var fg1 	= '#557799';
			var fg2 	= '#557799';
			var links 	= '#AABBCC';
			var accent1	= '#6699CC';
			var accent2	= '#6699CC';
			break;

		case 'classic':
			var bg1		= '#FFFFFF';
			var bg2		= '#3366CC';
			var fg1 	= '#666666';
			var fg2 	= '#AACCEE';
			var links 	= '#AACCEE';
			var accent1	= '#3366CC';
			var accent2	= '#FFFFFF';
			break;

		case 'green':
			var bg1		= '#FFFFFF';
			var bg2		= '#222222';
			var fg1 	= '#888888';
			var fg2 	= '#888888';
			var links 	= '#AAAAAA';
			var accent1	= '#33AA00';
			var accent2	= '#66CC33';
			break;

		case 'default':
		default:
			var bg1		= '#222222';
			var bg2		= '#111111';
			var fg1 	= '#888888';
			var fg2 	= '#888888';
			var links 	= '#AAAAAA';
			var accent1	= '#3380CC';
			var accent2	= '#3380CC';
			break;
	}

	/**
	 * Darkens a color by a specified amount
	 * @param	String	The RGB hex color code
	 * @param	Int		The amount (decimal-format pertentage) by which to darken the color
	 * @return	String	The color code for the darkened color
	 */
	function darken(color, amount) {
		color = splitColor(color);
		var ret = [];
		for (var i = 0; i < color.length; i++) {
			ret[i] = (color[i] - Math.ceil(255 * amount));

			if (ret[i] < 0)
				ret[i] = 0;
			if (ret[i] > 255)
				ret[i] = 255;

			ret[i] = ret[i].toString(16);
			if (ret[i].length < 2)
				ret[i] = '0' + ret[i];
		}
		return '#' + ret.join('').toUpperCase();
	}

	/**
	 * Lightens a color by a specified amount
	 * @param	String	The RGB hex color code
	 * @param	Int		The amount (decimal-format pertentage) by which to lighten the color
	 * @return	String	The color code for the lightened color
	 */
	function lighten(color, amount) {
		color = splitColor(color);
		var ret = [];
		for (var i = 0; i < color.length; i++) {
			ret[i] = (color[i] + Math.ceil(255 * amount));

			if (ret[i] < 0)
				ret[i] = 0;
			if (ret[i] > 255)
				ret[i] = 255;

			ret[i] = ret[i].toString(16);
			if (ret[i].length < 2)
				ret[i] = '0' + ret[i];
		}
		return '#' + ret.join('').toUpperCase();
	}

	/**
	 * Converts a color code into a usable array for math-based functions
	 * @param	String	The RGB hex color code
	 * @return	Int[]	The array containing the decimal values of each color
	 */
	function splitColor(color) {
		color = color.replace('#', '');

		var offset = Math.floor(color.length / 3);
		var ret = [];
		for (var i = 0; i < color.length; i+=offset) {
			ret.push(parseInt(color.substring(i, (i + offset)), 16));
		}
		return ret;
	}

	/**
	 * Returns the CSS for a vertical background gradient (with vendor-specific prefixes)
	 * @param	String	The RGB hex color code of the top color
	 * @param	String	The RGB hex color code of the bottom color
	 * @return	String	The CSS for the background gradient
	 */
	function gradient(top, bottom) {
		var ret = '';
		ret += 'background: -moz-linear-gradient(top, ' + top + ' 0%, ' + bottom + ' 100%);';
		ret += 'background: -webkit-linear-gradient(top, ' + top + ' 0%, ' + bottom + ' 100%);';
		return ret;
	}

	/**
	 * Returns the CSS for rounded corners (with vendor-specific prefixes)
	 * @param	String	The radius value (syntax: '#px' or '#px #px #px #px')
	 * @return	String	The CSS for the rounded corners
	 */
	function borderRadius(radius) {
		radius = 'border-radius: ' + radius;
		// returns: -moz-border-radius: <radius>; -webkit-border-radius: <radius>; border-radius: <radius>;
		return '-moz-' + radius + '; ' + '-webkit-' + radius + '; ' + radius + ';';
	}

	/**
	 * Returns the CSS for box shadows (with vendor-specific prefixes)
	 * @param	String	The shadow value (syntax: '#px #px [#px] [#px] color [inset]')
	 * @return	String	The CSS for the box shadows
	 */
	function boxShadow(shadow) {
		shadow = 'box-shadow: ' + shadow;
		// returns: -moz-box-shadow: <shadow>; -webkit-box-shadow: <shadow>; box-shadow: <shadow>;
		return '-moz-' + shadow + '; ' + '-webkit-' + shadow + '; ' + shadow + ';';
	}

	// basics
	css += '* { -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; margin: 0; padding: 0; }';
	css += 'body { background-color: ' + bg1 + '; color: ' + fg1 + '; font: 13px Helvetica, Arial, sans-serif; }';
	css += 'a { color: ' + links + '; cursor: pointer; text-decoration: underline; }';
	css += 'a:hover, a:hover { color: ' + lighten(links, 0.133) + '; text-decoration: none; }';
	css += 'p { margin: 0 0 10px; }';
	css += 'table { font: 13px Helvetica, Arial, sans-serif; margin: auto; width: 100%; }';

	// layout
	css += '#header { background-color: ' + bg2 + '; border-bottom: 1px solid ' + lighten(bg1, 0.066) + '; color: '+ fg2 + '; padding: 10px 0 0; text-align: center; }';
	css += '.scrolling #header { margin-bottom: 10px; }';
	css += '.slideshow #header { min-height: 60px; position: fixed; top: 0; width: 100%; z-index: 2; }';
	css += '#header .title { color: ' + accent2 + '; }';
	css += '#header small { font-variant: small-caps; }';
	css += '#header p { margin: 10px 0; }';
	css += '#header #description { margin: 10px auto; max-width: 60%; text-align: center; }';
	css += '#search { position: absolute; right: 10px; top: 10px; }';
	css += '#footer { background-color: ' + bg2 + '; border-top: 1px solid ' + lighten(bg1, 0.066) + '; color: '+ fg2 + '; min-height: 60px; padding: 10px 0; position: relative; text-align: center; }';
	css += '.scrolling #footer { margin-top: 10px; position: fixed; bottom: 0; left: 0; right: 0; }';
	css += '.slideshow #footer { bottom: 0; height: 60px; position: fixed; width: 100%; z-index: 2; }';
	css += '#favorites_container { height: 40px; line-height: 40px; margin-bottom: 0 !important; position: absolute; left: 25px; bottom: 10px; }';
	css += '#autoplay { height: 20px; line-height: 20px; position: absolute; right: 25px; bottom: 20px; }';
	css += '#info { font-size: 11px; margin: 10px 0 0; }';

	// logo
	css += '#logo { text-decoration: none; position: absolute; left: 10px; top: 10px; }';
	css += '#logo span { font: 18px "Comic Sans MS"; padding: 0 2px; }';
	css += '#logo .image { background-color: ' + bg1 + '; color: ' + accent1 + '; }';
	css += '#logo:hover .image { background-color: ' + accent1 + '; color: ' + bg1 + '; }';
	css += '#logo .fap { background-color: ' + accent1 + '; color: ' + bg1 + '; }';
	css += '#logo:hover .fap { background-color: ' + bg1 + '; color: ' + accent1 + '; }';

	// forms
	css += 'form { margin: 0 0 10px; }';
	css += 'input[type="text"], select { background: ' + bg1 + '; border: 1px solid ' + fg1 + '; color: ' + fg1 + '; margin-right: 5px; padding: 5px; }';
	css += 'input[type="text"]:focus, select:focus { border-color: ' + accent1 + '; color: ' + fg1 + '; }';
	css += 'button, input[type="button"], input[type="submit"] { background: ' + lighten(bg2, 0.066) + '; ' + gradient(lighten(bg2, 0.133), lighten(bg2, 0.066)) + '; border: 1px solid ' + lighten(bg2, 0.200) + '; ' + borderRadius('5px') + ' color: ' + links + '; cursor: pointer; margin-left: 5px; padding: 5px; ' + boxShadow('0 0 0 1px ' + bg2) + '; }';
	css += 'button:hover, input[type="button"]:hover, input[type="submit"]:hover { ' + gradient(lighten(bg2, 0.200), lighten(bg2, 0.066)) + '; border: 1px solid ' + lighten(bg2, 0.266) + '; color: ' + lighten(links, 0.066) + '; }';
	css += 'button:focus, input[type="button"]:focus, input[type="submit"]:focus { ' + gradient(lighten(bg2, 0.266), lighten(bg2, 0.066)) + '; border: 2px solid ' + lighten(bg2, 0.333) + '; color: ' + lighten(links, 0.133) + '; padding: 4px; }';

	css += '#favorites_container input[type="button"], button.default, input[type="submit"] { border: 1px solid ' + accent1 + '; color: ' + lighten(links, 0.133) + '; }';
	css += '#favorites_container input[type="button"]:hover, button.default:hover, input[type="submit"]:hover { border: 1px solid ' + lighten(accent1, 0.066) + '; color: ' + lighten(links, 0.200) + '; }';
	css += '#favorites_container input[type="button"]:focus, button.default:focus, input[type="submit"]:focus { border: 2px solid ' + lighten(accent1, 0.066) + '; color: ' + lighten(links, 0.266) + '; padding: 4px; }';

	// pagination
	css += '.pagination { margin: 0; }';
	css += '.pagination a { border: 1px solid ' + darken(links, 0.133) + '; color: ' + darken(links, 0.133) + '; display: inline-block; margin: 0 2px 10px; padding: 2px 6px; text-decoration: none; }';
	css += '.pagination a:hover { border-color: ' + links + '; color: ' + links + '; display: inline-block; margin: 0 2px; padding: 2px 6px; text-decoration: none; }';
	css += '.pagination a.current { border: 1px solid ' + accent2 + '; color: ' + accent2 + '; }';
	css += '.pagination a.disabled { border: 1px solid ' + darken(fg2, 0.266) + '; color: ' + darken(fg2, 0.133) + '; cursor: default; display: inline-block; margin: 0 2px; padding: 2px 6px; }';

	// thumbnails
	css += '#thumbnails { margin-bottom: 10px; text-align: center; z-index: 2; }';
	css += '#thumbnails .thumbnail { border: 1px solid ' + darken(links, 0.133) + '; display: inline-block; margin: 2px; padding: 4px; vertical-align: middle; }';
	css += '#thumbnails .thumbnail:hover { border-color: ' + links + '; }';
	css += '#thumbnails .thumbnail.active { border: 2px solid ' + accent2 + '; padding: 3px; }';
	css += '#thumbnails.small img { max-height: 100px; }';
	css += '#thumbnails.medium img { max-height: 150px; }';
	css += '#thumbnails.large img { max-height: 200px; }';
	css += '.slideshow #thumbnails { background-color: ' + bg2 + '; padding-bottom: 10px; overflow-y: hidden; width: 100%; white-space: nowrap; }';
	css += '.slideshow #thumbnails .thumbnail { margin: 0 5px; }';

	// gallery basics
	css += '#gallery { position: relative; text-align: center; }';
	css += '#gallery .image img { border: 1px solid ' + darken(links, 0.133) + '; padding: 4px; min-height: 100px; min-width: 100px; }';
	css += '#gallery .image:hover img { border-color: ' + accent1 + '; }';
	css += '#gallery .image .spinner { border: 10px solid ' + darken(fg2, 0.266) + '; border-left-color: ' + accent1 + '; position: absolute; left: calc(100% / 2 - 80px / 2); top: calc(100% / 2 - 80px / 2); -webkit-animation: spinning 1s infinite linear; animation: spinning 1s infinite linear; }';
	css += '#gallery .image .spinner, #gallery .image .spinner:after { ' + borderRadius('50%') + '; width: 80px; height: 80px; z-index: -1; }';
	css += '@-webkit-keyframes spinning { 0% { -webkit-transform: rotate(0deg); transform: rotate(0deg); } 100% { -webkit-transform: rotate(360deg); transform: rotate(360deg); } }';
	css += '@keyframes spinning { 0% { -webkit-transform: rotate(0deg); transform: rotate(0deg); } 100% { -webkit-transform: rotate(360deg); transform: rotate(360deg); } }';

	// scrolling gallery
	css += '.scrolling #gallery { max-width: 100%; }';
	css += '.scrolling #gallery .image { clear: both; display: inline-block; max-width: 98%; position: relative; }';
	css += '.scrolling #gallery .image img { display: inline-block; max-width: 100%; }';

	css += '#loader { background: ' + lighten(bg2, 0.066) + '; ' + gradient(lighten(bg2, 0.133), lighten(bg2, 0.066)) + '; border: 1px solid ' + accent1 + '; border-radius: 5px; ' + boxShadow('0 0 0 1px ' + bg2) + '; color: ' + lighten(links, 0.133) + '; display: inline-block; margin-top: 10px; padding: 8px 16px; text-decoration: none; }';
	css += '#loader:hover { ' + gradient(lighten(bg2, 0.200), lighten(bg2, 0.066)) + '; border-color: ' + lighten(accent1, 0.066) + '; color: ' + lighten(links, 0.200) + '; }';

	// slideshow gallery
	css += '.slideshow #content { bottom: 81px; left: 0; padding: 10px; position: absolute; right: 0; top: 81px; }';
	css += '.slideshow #gallery { height: 100%; width: 100%; overflow: hidden; }';
	css += '.slideshow #gallery .image img { bottom: 0; left: 0; margin: auto; max-height: 100%; max-width: 100%; position: absolute; right: 0; top: 0; }';
	css += '.slideshow #gallery .nav { color: #FFFFFF; display: block; text-decoration: none; opacity: 0.5; position: absolute; top: 5px; bottom: 5px; height: 100%; width: 160px; z-index: 1; }';
	css += '.slideshow #gallery .nav.disabled { display: none; }';
	css += '.slideshow #gallery #next { min-height: 60px; min-width: 60px; right: 5px; }';
	css += '.slideshow #gallery #prev { min-height: 60px; min-width: 60px; left: 5px; }';
	css += '.slideshow #gallery .arrow { display: block; font-size: 120px; height: 160px; line-height: 160px; margin-top: -80px; position: relative; text-align: center; text-shadow: 0 0 5px #000000, 0 0 20px #FFFFFF; top: 50%; }';
	css += '.slideshow #gallery .nav:hover { background-color: rgba(0,0,0,0.5); opacity: 1.0; }';
	css += '.slideshow #gallery #indicator { background-color: rgba(0,0,0,0.5); ' + borderRadius('40px') + '; display: block; position: absolute; top: 50%; left: 50%; height: 160px; margin-top: -80px; margin-left: -80px; width: 160px; z-index: 1; }';
	css += '.slideshow #gallery #indicator .symbol { color: #FFFFFF; font-size: 120px; position: relative; text-align: center; text-shadow: 0 0 5px #000000, 0 0 20px #FFFFFF; }';
	css += '.slideshow #gallery #indicator .symbol.pause { font-weight: bold; }';
	css += '.slideshow #gallery #indicator .symbol.play { line-height: 160px; }';

	// full screen:: WebKit
	css += ':-webkit-full-screen { background-color: #000000; }';
	css += '#content:-webkit-full-screen { padding: 0; top: 0 !important; bottom: 0 !important; height: 100%; width: 100%; }';
	css += '#content:-webkit-full-screen .nav { ' + borderRadius('40px') + '; height: 160px; margin-top: -80px; top: 50%; }';
	css += '#content:-webkit-full-screen .image img { border: 0; padding: 0; }';
	css += '#content:-webkit-full-screen #thumbnails { background-color: #000000; position: absolute; top: 0; }';
	// full screen:: Mozilla
	css += '#content:-moz-full-screen { padding: 0; top: 0 !important; bottom: 0 !important; height: 100%; width: 100%; }';
	css += '#content:-moz-full-screen .nav { ' + borderRadius('40px') + '; height: 160px; margin-top: -80px; top: 50%; }';
	css += '#content:-moz-full-screen .image img { border: 0; padding: 0; }';
	css += '#content:-moz-full-screen #thumbnails { background-color: #000000; position: absolute; top: 0; }';

	// add to favorites
	css += '#favorites_container table { width: auto; }';

	// notification messages
	css += '#notificationContainer { bottom: 70px; position: fixed; right: 10px; }';
	css += '.notification { background: rgba(0, 0, 0, 0.8); border: 1px solid rgba(255, 255, 255, 0.2); ' + borderRadius('5px') + '; color: rgba(255, 255, 255, 0.5); display: block; font-size: 11px; margin-top: 10px; padding: 9px; }';

	// dialog box layout
	css += '#dialogContainer { background: rgba(0,0,0,0.8); display: block; text-align: center; position: fixed; top: 0; bottom: 0; left: 0; right: 0; z-index: 3; overflow-y: auto; }';
	css += '#dialogBox { background: ' + bg1 + '; border: 1px solid ' + lighten(bg2, 0.200) + '; ' + boxShadow('0 0 20px 0 #000000') + '; color: ' + fg1 + '; display: inline-block; margin: 20px auto; min-width: 300px; text-align: left; position: relative; z-index: 10; }';
	css += '#dialogClose { border-left: 1px solid ' + lighten(bg2, 0.200) + '; color: ' + links + '; font-size: 14px; line-height: 28px; position: absolute; right: 0; text-align: center; text-decoration: none; top: 0; width: 30px; }';
	css += '#dialogClose:hover { color: ' + lighten(links, 0.200) + '; }';
	css += '#dialogTitle { background: ' + bg1 + '; ' + gradient(lighten(bg2, 0.066), bg2) + '; border-bottom: 1px solid ' + lighten(bg2, 0.200) + '; color: ' + links + '; display: block; margin: 0; padding: 5px 10px; }';
	css += '#dialogTitle b { color: ' + accent2 + '; }';
	css += '#dialogMessage { display: block; padding: 10px; }';
	css += '#dialogButtons { clear: both; display: block; padding: 10px; text-align: right; }';

	// dialog box content
	css += '#dialogMessage table { margin: 0; }';
	css += '#dialogMessage table th { color: ' + accent1 + '; font-size: 14px; text-align: left; }';
	css += '#dialogMessage table b { color: ' + lighten(accent1, 0.066) + '; }';
	css += '#dialogMessage table td.name { padding-right: 5%; text-align: right; width: 20%; }';
	css += '#dialogMessage table td.key { text-align: center; width: 25%; }';

	// dialog box form elements
	css += '#dialogBox #settingsForm { width: 820px }';
	css += '#dialogBox .error { color: #C43131; font-weight: bold; }';
	css += '#dialogBox button { margin-right: 5px; }';
	css += '#dialogBox fieldset { border: 0; border-bottom: 1px solid ' + lighten(bg2, 0.200) + '; margin: 0 0 15px; padding-bottom: 10px; min-width: 400px; }';
	css += '#dialogBox fieldset:nth-child(1n) { float: left; }';
	css += '#dialogBox fieldset:nth-child(2n) { float: right; }';
	css += '#dialogBox legend { color: ' + accent1 + '; font-weight: bold; margin-bottom: 10px; }';
	css += '#dialogBox label { float: left; line-height: 20px; }';
	css += '#dialogBox label b { color: ' + accent1 + '; }';
	css += '#dialogBox label span.default { font-size: 11px; font-variant: small-caps; }';
	css += '#dialogBox input[type="text"], #dialogBox select { float: right; }';
	css += '#dialogBox input[type="checkbox"] { float: left; margin: 4px 10px 0 ; }';

	// insert the line breaks automatically before returning
	return css.replace(/}/g, "}\n");
}