MassiveFap

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

Fra 19.04.2021. Se den seneste versjonen.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

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