User:DieBuche/Gadget-GallerySlideshow.js

Note: After saving, you have to bypass your browser's cache to see the changes. Internet Explorer: press Ctrl-F5, Mozilla: hold down Shift while clicking Reload (or press Ctrl-Shift-R), Opera/Konqueror: press F5, Safari: hold down Shift + Alt while clicking Reload, Chrome: hold down Shift while clicking Reload.
/**
 * jQuery Galleriffic plugin
 *
 * Copyright (c) 2008 Trent Foley (http://trentacular.com)
 * Licensed under the MIT License:
 *   http://www.opensource.org/licenses/mit-license.php
 *
 * Much thanks to primary contributer Ponticlaro (http://www.ponticlaro.com)
 *
 * Rewritten for commons by [[User:DieBuche]],
 * Based on a script by [[User:Dschwen]]
 */
 
if (typeof(GallerySlide) === 'undefined' && mw.config.get('wgNamespaceNumber') === 14) {
 
importStylesheet('User:DieBuche/Gadget-GallerySlideshow.css');
 
//Declare global var for todays specific gallery
var GallerySlide;
 
(function ($) {
	// Globally keep track of all images by their unique hash.  Each item is an image data object.
	var allImages = {};
	var imageCounter = 0;
 
	// Galleriffic static class
	$.galleriffic = {
		version: '2.1',
 
		// Strips invalid characters and any leading # characters
		normalizeHash: function (hash) {
			return hash.replace('#', '');
		},
 
		getImage: function (hash) {
			if (!hash) return undefined;
 
			hash = $.galleriffic.normalizeHash(hash);
			return allImages[hash];
		},
 
		// Global function that looks up an image by its hash and displays the image.
		// Returns false when an image is not found for the specified hash.
		// @param {String} hash This is the unique hash value assigned to an image.
		gotoImage: function (hash) {
			var imageData = $.galleriffic.getImage(hash);
			//if (!imageData) return false;
			var gallery = imageData.gallery;
			gallery.gotoImage(imageData);
 
			return true;
		}
 
	};
 
	var defaults = {
		delay: 5500,
		delayInsertBtn: 'Set delay in ms',
		delayInsert: 'How many ms to wait for a new image?',
		delayInvalid: 'Invalid input. Only numbers greater than 2500 are accepted.',
		preloadAhead: 25,
		playLinkText: 'Play',
		playLinkIcon: '//upload.wikimedia.org/wikipedia/commons/thumb/9/93/Gnome-media-playback-start.svg/35px-Gnome-media-playback-start.svg.png',
		pauseLinkText: 'Pause',
		pauseLinkIcon: '//upload.wikimedia.org/wikipedia/commons/thumb/0/04/Gnome-media-playback-pause.svg/35px-Gnome-media-playback-pause.svg.png',
		prevLinkText: 'Previous',
		prevLinkIcon: '//upload.wikimedia.org/wikipedia/commons/thumb/9/92/Gnome-media-skip-backward.svg/35px-Gnome-media-skip-backward.svg.png',
		nextLinkText: 'Next',
		nextLinkIcon: '//upload.wikimedia.org/wikipedia/commons/thumb/7/73/Gnome-media-skip-forward.svg/35px-Gnome-media-skip-forward.svg.png',
		hideText: 'Close slideshow',
		continueKeyHowTo: 'Continue Key - You can save this key or bookmark the link if you\'d like to start at this position later.',
		continueKeyInsert: 'Please insert the continue-key. You have to go forward in the slideshow to see effect.',
		continueKeyInsertBtn: 'Insert continue-key',
		continueKeyInvalid: 'Invalid key.',
		licenseLabel: 'Available Licenses: ',
		helpLinkTitle: 'Help and documentation for this tool',
		descriptionLoadText: 'Loading description ...',
		descriptionSpinner: '//bits.wikimedia.org/skins-1.5/common/images/ajax-loader.gif',
		enableKeyboardNavigation: true,
		autoPlay: false,
		defaultTransitionDuration: 700,
		maxImageHeight: Math.floor(($(window).height() - 120)/50) * 50, // using Interval-Sizes to reduce server-load (cached thumbs)
		maxImageWidth: Math.floor(($(window).width() - 400)/50) * 50,
		cmdir: 'asc',
		continueKey: '',
		continueKeyPattern: /^file\|[\da-fA-F]+\|\d+$/,
		licenseRecognization: [
			// RegExp for the tag			note to add to the thumb-page
			[/Category:CC[\- _]BY-SA.*/i,	'CC-By-SA'],
			[/Category:CC[\- _]BY.*/i,		'CC-By'],
			[/Category:CC[\- _]Zero.*/i,	'CC0'],
			[/Category:GFDL.*/i,			'GFDL'],
			[/Category:PD[\- _]Old.*/i,		'PD-old'],
			[/Category:PD[\- _]self.*/i,	'PD-self'],
			[/Category:PD[\- _]author.*/i,	'PD-author'],
			[/Category:PD.*/i,				'PDx'],
			[/Category:FAL/i,				'Art Libre - Free Art'],
			[/Category:Images requiring attribution/i,		'Attribution'],
			[/Category:Copyrighted free use.*/i,			'Copyrighted FreeUse'],
			[/Category:Mozilla Public License/i,			'MPL'],
			[/Category:GPL/i,								'GPL'],
			[/Category:LGPL/i,								'LGPL'],
			[/Category:Free screenshot.*/i,					'free-Screenshot']
		],
		onSlideChange: function (prevIndex, nextIndex) {
 
			var displayed = Math.floor(this.$thumbsUl.parent().width() / 81);
			var spaceRight = displayed - (nextIndex - this.hiddenLeft);
			var spaceLeft = (1 + nextIndex - this.hiddenLeft);
			if (spaceRight < 3) {
				//Time to slide viewport
				var current = this.$thumbsUl.css('left').replace('px', '');
				var offset = (parseFloat(current) - (3 - spaceRight) * 81);
				this.$thumbsUl.animate({
					left: offset + 'px'
				}, 'fast');
				this.hiddenLeft = this.hiddenLeft + (3 - spaceRight);
			}
			if (spaceLeft < 3 && nextIndex > 1) {
				var current = this.$thumbsUl.css('left').replace('px', '');
				var offset = (parseFloat(current) + (3 - spaceLeft) * 81);
				this.$thumbsUl.animate({
					left: offset + 'px'
				}, 'fast');
				this.hiddenLeft = this.hiddenLeft - (3 - spaceLeft);
			}
 
			if (nextIndex === 0) {
				this.$thumbsUl.animate({
					left: '0px'
				}, 'fast');
				this.hiddenLeft = 0;
			}
 
			if (this.data.length - 5 < nextIndex) {
				//Time to fetch more
				GallerySlide.queryApi();
			}
		},
		// accepts a delegate like such: function(prevIndex, nextIndex) { ... }
		onTransitionOut: undefined,
		// accepts a delegate like such: function(slide, caption, isSync, callback) { ... }
		onTransitionIn: undefined
	};
 
	// Primary Galleriffic initialization function that should be called on the thumbnail container.
	$.fn.galleriffic = function (settings) {
		//  Extend Gallery Object
		$.extend(this, {
			// Returns the version of the script
			version: $.galleriffic.version,
 
			// Current state of the slideshow
			isSlideshowRunning: false,
			slideshowTimeout: undefined,
			hiddenLeft: 0,
			apiURL: (/^\/\//.test(mw.config.get("wgServer")) ? document.location.protocol : '') + mw.config.get("wgServer") + mw.util.wikiScript( 'api' ),
			indexURL: mw.config.get('wgServer') + mw.config.get('wgScript'),
			initial: true,
			data: [],
 
			// This function is attached to the click event of generated hyperlinks within the gallery
			clickHandler: function (e, link) {
				this.pause();
 
				// The href attribute holds the unique hash for an image
				var hash = $.galleriffic.normalizeHash($(link).attr('href'));
				$.galleriffic.gotoImage(hash);
				e.preventDefault();
 
			},
 
			createContainer: function () {
				var gallery = this;
				
				this.$slideshowContainer = $('<div class="slideshow-container"></div>');
 				this.$imageContainer = $('<div id="slideshow" class="slideshow"></div>');
				this.$captionContainer = $('<div id="caption" class="caption-container"></div>');
				this.$loadingContainer = $('<div id="loading" class="loader"></div>');
				this.$controlsContainer = $('<div id="controls" class="controls"></div>');
				// Gray lines for navigation
				this.$ctrBack = $('<div>', { 'class': 'bar-rwd' } )
					.append($('<div>', { 'class': 'bar-btn-rwd' }).text('<'))
					.click(function (e) { gallery.previous(); e.preventDefault(); })
					.mouseenter(function () { $(this).find('div').fadeIn('fast'); }).mouseleave(function () { $(this).find('div').fadeOut('fast'); });
				this.$ctrFwd = $('<div>', { 'class': 'bar-fwd' } )
					.append( $('<div>', { 'class': 'bar-btn-fwd' }).text('>'))
					.click(function (e) { gallery.next(); e.preventDefault(); })
					.mouseenter(function () { $(this).find('div').last().fadeIn('fast'); }).mouseleave(function () { $(this).find('div').last().fadeOut('fast'); });
 				this.$closeButton = $('<div>', { 'class': 'slideshow-close-button', 'title' : this.hideText } )
					.text('×').click(function (e) {
						gallery.pause();
						gallery.toggleVisibility();
						e.preventDefault();
					});						
				
				this.append('<div id="thumbs" class="navigation"><ul class="thumbs"></ul></div>');
				this.append(this.$controlsContainer).append( this.$slideshowContainer );
				this.$slideshowContainer.append(this.$loadingContainer).append(this.$captionContainer).append(this.$imageContainer);
 				this.append(this.$ctrBack).append(this.$ctrFwd.prepend(this.$closeButton));
				this.$thumbsUl = this.find('ul.thumbs');
			},
			// Scrapes the thumbnail container for thumbs and adds each to the gallery
			initializeThumbs: function () {
 
				var data = this.passedData;
 
				var gallery = this;
 
				for (i in data) {
 
					imageData = data[i];
 
					imageData.index = hash = imageCounter;
					imageData.gallery = gallery;
 
					var aspect = (imageData.width / imageData.height);
					var size = (aspect > 1) ? 'height' : 'width';
					var $thumb = $('<li><a class="thumb"><img ' + size + '=75px src="' + imageData.slideUrl + '" /></a></li>');
					$thumb.css('opacity', 0.67).hover(
 
					function () {
						$(this).not('.selected').fadeTo('fast', 1);
					}, function () {
						$(this).not('.selected').fadeTo('fast', 0.67);
					});
 
					this.$thumbsUl.append($thumb);
 
					imageData.caption = $('<div>').append( $('<a>', { href: imageData.link }).text(imageData.title.replace('File:', '').replace(/\.[\w]{3,4}$/, '')) ).html();
 
					// Register the image globally
					allImages['' + hash] = imageData;
 
					// Setup attributes and click handler
					$thumb.find('a').attr('href', '#' + hash).removeAttr('name').click(function (e) {
						gallery.clickHandler(e, this);
					});
					imageCounter++;
 
				}
				this.data = this.data.concat(data);
 
				return this;
			},
 
			isPreloadComplete: false,
 
			// Initalizes the image preloader
			preloadInit: function () {
				if (this.preloadAhead === 0) return this;
 
				this.preloadStartIndex = this.currentImage.index;
				var nextIndex = this.getNextIndex(this.preloadStartIndex);
				return this.preloadRecursive(this.preloadStartIndex, nextIndex);
			},
 
			// Changes the location in the gallery the preloader should work
			// @param {Integer} index The index of the image where the preloader should restart at.
			preloadRelocate: function (index) {
				// By changing this startIndex, the current preload script will restart
				this.preloadStartIndex = index;
				return this;
			},
 
			// Recursive function that performs the image preloading
			// @param {Integer} startIndex The index of the first image the current preloader started on.
			// @param {Integer} currentIndex The index of the current image to preload.
			preloadRecursive: function (startIndex, currentIndex) {
				// Check if startIndex has been relocated
				if (startIndex != this.preloadStartIndex) {
					var nextIndex = this.getNextIndex(this.preloadStartIndex);
					return this.preloadRecursive(this.preloadStartIndex, nextIndex);
				}
 
				var gallery = this;
 
				// Now check for preloadAhead count
				var preloadCount = currentIndex - startIndex;
				if (preloadCount < 0) preloadCount = this.data.length - 1 - startIndex + currentIndex;
				if (this.preloadAhead >= 0 && preloadCount > this.preloadAhead) {
					// Do this in order to keep checking for relocated start index
					setTimeout(function () {
						gallery.preloadRecursive(startIndex, currentIndex);
					}, 500);
					return this;
				}
 
				var imageData = this.data[currentIndex];
				if (!imageData) return this;
 
				// If already loaded, continue
				if (imageData.image) return this.preloadNext(startIndex, currentIndex);
 
				// Preload the image
				var image = new Image();
 
				image.onload = function () {
					imageData.image = this;
					gallery.preloadNext(startIndex, currentIndex);
				};
 
				image.alt = imageData.title;
				image.src = imageData.slideUrl;
 
				return this;
			},
 
			// Called by preloadRecursive in order to preload the next image after the previous has loaded.
			// @param {Integer} startIndex The index of the first image the current preloader started on.
			// @param {Integer} currentIndex The index of the current image to preload.
			preloadNext: function (startIndex, currentIndex) {
				var nextIndex = this.getNextIndex(currentIndex);
				if (nextIndex === startIndex) {
					this.isPreloadComplete = true;
				} else {
					// Use setTimeout to free up thread
					var gallery = this;
					setTimeout(function () {
						gallery.preloadRecursive(startIndex, nextIndex);
					}, 100);
				}
 
				return this;
			},
 
			// Safe way to get the next image index relative to the current image.
			// If the current image is the last, returns 0
			getNextIndex: function (index) {
				var nextIndex = index + 1;
				if (nextIndex >= this.data.length) nextIndex = 0;
				return nextIndex;
			},
 
			// Safe way to get the previous image index relative to the current image.
			// If the current image is the first, return the index of the last image in the gallery.
			getPrevIndex: function (index) {
				var prevIndex = index - 1;
				if (prevIndex < 0) prevIndex = this.data.length - 1;
				return prevIndex;
			},
 
			// Pauses the slideshow
			pause: function () {
				this.isSlideshowRunning = false;
				if (this.slideshowTimeout) {
					clearTimeout(this.slideshowTimeout);
					this.slideshowTimeout = undefined;
				}
 
				if (this.$controlsContainer) {
					this.$controlsContainer.find('div.nav-controls a.pause').removeClass().addClass('play').attr('title', this.playLinkText).attr('href', '#play')
						.html('<img src="' + this.playLinkIcon + '" height="35" width="35" alt="' + this.playLinkText + '"/>');
				}
 
				return this;
			},
 
			// Plays the slideshow
			play: function () {
				this.isSlideshowRunning = true;
 
				if (this.$controlsContainer) {
					this.$controlsContainer.find('div.nav-controls a.play').removeClass().addClass('pause').attr('title', this.pauseLinkText).attr('href', '#pause')
						.html('<img src="' + this.pauseLinkIcon + '" height="35" width="35" alt="' + this.pauseLinkText + '"/>');
				}
 
				if (!this.slideshowTimeout) {
					var gallery = this;
					this.slideshowTimeout = setTimeout(function () {
						gallery.next(true);
					}, this.delay);
				}
 
				return this;
			},
 
			// Toggles the state of the slideshow (playing/paused)
			toggleSlideshow: function () {
				if (this.isSlideshowRunning) this.pause();
				else this.play();
 
				return this;
			},
			// Advances the gallery to the next image.
			// @param {Boolean} dontPause Specifies whether to pause the slideshow.
			next: function (dontPause) {
				this.gotoIndex(this.getNextIndex(this.currentImage.index), dontPause);
				return this;
			},
 
			// Navigates to the previous image in the gallery.
			// @param {Boolean} dontPause Specifies whether to pause the slideshow.
			previous: function (dontPause) {
				this.gotoIndex(this.getPrevIndex(this.currentImage.index), dontPause);
				return this;
			},
 
			// Navigates to the image at the specified index in the gallery
			// @param {Integer} index The index of the image in the gallery to display.
			// @param {Boolean} dontPause Specifies whether to pause the slideshow.
			gotoIndex: function (index, dontPause) {
				if (!dontPause) this.pause();
 
				if (index < 0) index = 0;
				else if (index >= this.data.length) index = this.data.length - 1;
 
				var imageData = this.data[index];
 
				this.gotoImage(imageData);
 
				return this;
			},
 
			// This function is guaranteed to be called anytime a gallery slide changes.
			// @param {Object} imageData An object holding the image metadata of the image to navigate to.
			gotoImage: function (imageData) {
				var index = imageData.index;
 
				if (this.onSlideChange) this.onSlideChange(this.currentImage.index, index);
 
				this.currentImage = imageData;
				this.preloadRelocate(index);
 
				this.refresh();
 
				return this;
			},
 
			// Returns the default transition duration value.  The value is halved when not
			// performing a synchronized transition.
			// @param {Boolean} isSync Specifies whether the transitions are synchronized.
			getDefaultTransitionDuration: function (isSync) {
				if (isSync) return this.defaultTransitionDuration;
				return this.defaultTransitionDuration / 2;
			},
 
			// Rebuilds the slideshow image and controls and performs transitions
			refresh: function () {
				var imageData = this.currentImage;
				if (!imageData) return this;
 
				var index = imageData.index;
 
				var previousSlide = this.$imageContainer.find('span.current').addClass('previous').removeClass('current');
				var previousCaption = 0;
 
				if (this.$captionContainer) {
					previousCaption = this.$captionContainer.find('span.current').addClass('previous').removeClass('current');
				}
 
				// Perform transitions simultaneously if the next image is already preloaded
				var isSync = imageData.image;
 
				// Flag we are transitioning
				var isTransitioning = true;
				var gallery = this;
 
				var transitionOutCallback = function () {
					// Flag that the transition has completed
					isTransitioning = false;
 
					// Remove the old slide
					previousSlide.remove();
 
					// Remove old caption
					if (previousCaption) previousCaption.remove();
 
					if (!isSync) {
						if (imageData.image && imageData.index === gallery.data[gallery.currentImage.index].index) {
							gallery.buildImage(imageData, isSync);
						} else {
							// Show loading container
							if (gallery.$loadingContainer) {
								gallery.$loadingContainer.show();
							}
						}
					}
				};
 
				if (previousSlide.length === 0) {
					// For the first slide, the previous slide will be empty, so we will call the callback immediately
					transitionOutCallback();
				} else {
 
					previousSlide.fadeTo(this.getDefaultTransitionDuration(isSync), 0.0, transitionOutCallback);
					if (previousCaption) previousCaption.fadeTo(this.getDefaultTransitionDuration(isSync), 0.0);
 
				}
 
				// Go ahead and begin transitioning in of next image
				if (isSync) this.buildImage(imageData, isSync);
 
				if (!imageData.image) {
					var image = new Image();
 
					// Wire up mainImage onload event
					image.onload = function () {
						imageData.image = this;
 
						// Only build image if the out transition has completed and we are still on the same image hash
						if (!isTransitioning && imageData.index === gallery.data[gallery.currentImage.index].index) {
							gallery.buildImage(imageData, isSync);
						}
					};
 
					// set alt and src
					image.alt = imageData.title;
					image.src = imageData.slideUrl;
				}
 
				// This causes the preloader (if still running) to relocate out from the currentIndex
				this.relocatePreload = true;
 
				return this.syncThumbs();
			},
 
			// Called by the refresh method after the previous image has been transitioned out or at the same time
			// as the out transition when performing a synchronous transition.
			// @param {Object} imageData An object holding the image metadata of the image to build.
			// @param {Boolean} isSync Specifies whether the transitions are synchronized.
			buildImage: function (imageData, isSync) {
				var gallery = this;
				var nextIndex = this.getNextIndex(imageData.index);
 
				// computing the "center-position of the space"
				var hSpace = (this.$ctrFwd.position().left - (this.$ctrBack.width() + this.$ctrBack.position().left) - 370 - 10);
				var left = (this.$slideshowContainer.width() - this.$captionContainer.width() - imageData.width + 30 ) / 2;
				var vSpace = this.$ctrFwd.height();
				var top = ($(window).height() - this.$thumbsUl.height() - this.$controlsContainer.height() - imageData.height) / 2;
				
				// Send an API request in case of unknown description
				if ('string' !== typeof imageData.description) this.queryFile(imageData.title);
				if ('string' !== typeof this.data[nextIndex].description) this.queryFile(this.data[nextIndex].title);
 
				// Construct new hidden span for the image
				var newSlide = this.$imageContainer.append($('<span>', { 'class': 'image-wrapper current', style: 'left:' + left + 'px; top:' + top + 'px; ' }).append($('<a>', { 'class': 'advance-link', href: imageData.link, title: imageData.title, target: '_blank', style: 'width:' + imageData.width + 'px; ' } ))).find('span.current').css('opacity', '0');
 
				newSlide.find('a').append(imageData.image);
				
				var descript = imageData.description ? imageData.description : 
					$('<span>').append($('<img>', { src: this.descriptionSpinner, height: 20, width: 20 }), mw.html.escape(this.descriptionLoadText));
					
				var newCaption = 0;
				var extraParams = '&gsDir=' + this.cmdir + '&gsAutoStart=1' + (mw.util.getParamValue('withJS') ? ('&withJS=' + mw.util.getParamValue('withJS')) : '' );
				if (this.$captionContainer) {
					// Construct new hidden caption for the image
					newCaption = this.$captionContainer.append( $('<span>', { 'class': 'image-caption current', style: 'height:' + (this.maxImageHeight - 20) + 'px;' }) ).find('span.current').css('opacity', '0')
						.append(imageData.caption, $('<br>'))
						.append( $('<div>', { 'class': 'gs-img-description', id: 'desc' + imageData.index }).append(descript) )
						.append( $('<div>', { 'class': 'gs-img-uploader' }).append(imageData.$user.clone()) )
						.append( imageData.$cats, imageData.$licenses, $('<br>') )
						.append( $('<div>', { 'class': 'gs-img-metrics' }).html('(' + imageData.oWidth + '×' + imageData.oHeight + ' (' + imageData.oSize + '))') )
						.append( (imageData.contKey ? 
							$('<div>', { 'class': 'cont-key-container', title: this.continueKeyHowTo }).append( 
								$('<a>', { href: mw.util.getUrl(mw.config.get('wgPageName')) + '?gsContinue=' + imageData.contKey + extraParams, target: '_blank' } 
								).text(imageData.contKey) ) : ''
						) );
				}
 
				// Hide the loading conatiner
				if (this.$loadingContainer) {
					this.$loadingContainer.hide();
				}
 
				// Transition in the new image
				if (this.onTransitionIn) {
					this.onTransitionIn(newSlide, newCaption, isSync);
				} else {
					newSlide.fadeTo(this.getDefaultTransitionDuration(isSync), 1.0);
					if (newCaption) newCaption.fadeTo(this.getDefaultTransitionDuration(isSync), 1.0);
				}
 
				if (this.isSlideshowRunning) {
					if (this.slideshowTimeout) clearTimeout(this.slideshowTimeout);
 
					this.slideshowTimeout = setTimeout(function () {
						gallery.next(true);
					}, this.delay);
				}
				
				// Save the current position in a cookie or delete the cookie
				if (imageData.contKey) {
					mw.cookie.get( 'gs' + mw.config.get('wgPageName').replace('Category:', ''), imageData.contKey, { expires: 8 } );
				} else {
					mw.cookie.get( 'gs' + mw.config.get('wgPageName').replace('Category:', ''), null );
				}
				
				$(document).trigger('slideshow', ['newSlide', this]); // For external scripts
 
				return this;
			},
 
 
			// Applies the selected class to the current image's corresponding thumbnail.
			// Also checks if the current page has changed and updates the displayed page of thumbnails if necessary.
			syncThumbs: function () {
				// Remove existing selected class and add selected class to new thumb
				var $thumbs = this.$thumbsUl.children();
 
				$thumbs.filter('.selected').removeClass('selected').fadeTo('fast', 0.67);
				$thumbs.eq(this.currentImage.index).addClass('selected').fadeTo('fast', 1);
 
				return this;
			},
 
			init: function () {
				this.createContainer();
 				var that = this;
 				$(window).resize(function() {
  					that.maxImageHeight = $(window).height() - that.$thumbsUl.height() - that.$controlsContainer.height();
  					that.maxImageWidth = that.$slideshowContainer.width() - that.$captionContainer.width() + 20;
				}).resize();

				// Initialize the thumbails
				this.initializeThumbs();
 
				this.currentImage = this.data[0];
 
				var gallery = this;
 
				// Hide the loadingContainer
				this.$loadingContainer.hide();
				this.gotoIndex(0);
 
				// Setup controls
				var playBtn, nextBtn, prevBtn, navCont;
				if (this.autoPlay) {
					playBtn = $('<a href="#pause" class="pause" title="' + this.pauseLinkText + '">' + '<img src="' + this.playLinkIcon + '" height="35" width="35" alt="' + this.pauseLinkText + '"/>' + '</a>');
				} else {
					playBtn = $('<a href="#play" class="play" title="' + this.playLinkText + '">' + '<img src="' + this.playLinkIcon + '" height="35" width="35" alt="' + this.playLinkText + '"/>' + '</a>');
				}

				playBtn.click(function (e) {
					gallery.toggleSlideshow();
					e.preventDefault();
				});
				
				prevBtn = $('<a class="prev" href="#" title="' + this.prevLinkText + '">' + '<img src="' + this.prevLinkIcon + '" height="35" width="35" alt="' + this.prevLinkText + '"/>' + '</a>');
				nextBtn = $('<a class="next" href="#"  title="' + this.nextLinkText + '">' + '<img src="' + this.nextLinkIcon + '" height="35" width="35" alt="' + this.nextLinkText + '"/>' + '</a>');

				prevBtn.click(function (e) {
					gallery.previous();
					e.preventDefault();
				});
				nextBtn.click(function (e) {
					gallery.next();
					e.preventDefault();
				});
				navCont = $('<div class="nav-controls">');
				navCont.hover( function() { $(this).fadeTo('fast', 0.75) }, function() { $(this).fadeTo('fast', 0.4)  });
				this.$controlsContainer.append( navCont.append(prevBtn, playBtn, nextBtn));
				// Option icons
				this.$continueKey = $('<a>', { 'class': 'continue-key-insert', href: '#', title: gallery.continueKeyInsertBtn })
					.click( function (e) {
						e.preventDefault();
						var ckey = prompt(gallery.continueKeyInsert, gallery.cont ? gallery.cont : '' );
						ckey = $.trim(ckey);
						if (gallery.continueKeyPattern.test(ckey)) {
							gallery.cont = ckey;
						} else {
							alert(gallery.continueKeyInvalid);
						}
					});
				var $setDelay = $('<a>', { 'class': 'delay-insert', href: '#', title: gallery.delayInsertBtn })
					.click( function (e) {
						e.preventDefault();
						var delay = prompt(gallery.delayInsert, gallery.delay ? gallery.delay : '' );
						if (!delay) return;
						delay = $.trim( delay.replace(/ms|s/, '') );
						if (/^\d+$/.test(delay)) {
							if (delay > 2499) {
								gallery.delay = delay;
								return;
							} else if (3 < delay < 61) {
								gallery.delay = delay * 1000;
								return;
							}
						}
						alert(gallery.delayInvalid);
					});
				var $helpLink = $('<a>', { 'class': 'gs-help-link', href: mw.util.getUrl('Help:Slideshow'), title: gallery.helpLinkTitle, target: '_blank' });
				var otherCont = $('<div>', { 'class' : 'other-controls'} );
				this.$controlsContainer.append( otherCont.append( this.$continueKey, $setDelay, $helpLink ) );
				otherCont.hover( function() { $(this).fadeTo('fast', 1) }, function() { $(this).fadeTo('fast', 0.6)  });
				
				


 
 
				// Setup Keyboard Navigation
				if (this.enableKeyboardNavigation) {
					var hidingEnabled = true;
					$(document).keydown(function (e) {
						var key = e.charCode ? e.charCode : e.keyCode ? e.keyCode : 0;
						switch (key) {
						case 32:
							// space
							gallery.next();
							e.preventDefault();
							break;
						case 35:
							// End
							gallery.gotoIndex(gallery.data.length - 1);
							e.preventDefault();
							break;
						case 37:
							// left arrow
							gallery.previous();
							e.preventDefault();
							break;
						case 39:
							// right arrow
							gallery.next();
							e.preventDefault();
							break;
						}
					});
					$(document).keyup(function (e) {
						var key = e.charCode ? e.charCode : e.keyCode ? e.keyCode : 0;
						//Hide on escape
						if ($('#SlideContainer').height() && key == 27) gallery.toggleVisibility();
					});
				}
				// Auto start the slideshow
				if (this.autoPlay) this.play();
 
				// Kickoff Image Preloader after 1 second
				setTimeout(function () {
					gallery.preloadInit();
				}, 1000);
				
				$(document).trigger('slideshow', ['shown', this]); // For external scripts
			},
 
			start: function () {
				$(document).trigger('slideshow', ['starting', this]); // For external scripts
				
				$('#GallerySlideInit').hide();
				$('#GallerySlideContinue').hide();
				$('#SlideContainer').animate({
					height: '100%'
				});
				//Once done, hide scrollbar
				$('body').css('overflow', 'hidden');
				
				// Settings from URL
				var autoPlay = mw.util.getParamValue('gsAutoPlay');
				if (autoPlay) {
					if ('1' === autoPlay || 'true' === autoPlay || 'yes' === autoPlay || '-1' === autoPlay) {
						this.autoPlay = true;
					} else {
						this.autoPlay = false;
					}
				}
				var delay = mw.util.getParamValue('gsDelay');
				if (delay) {
					if (/^\d+$/.test(delay)) {
						if (delay > 2499) {
							this.delay = delay;
						} else if (3 < delay < 61) {
							this.delay = delay * 1000;
						}
					}
				}
				var cmdir = mw.util.getParamValue('gsDir');
				if (cmdir) {
					if ('climbing' === cmdir || 'ascending' === cmdir || 'asc' === cmdir || '123' === cmdir || 'rising' === cmdir) {
						this.cmdir = 'asc';
					} else {
						this.cmdir = 'desc';
					}
				}
				var cmcontinue = mw.util.getParamValue('gsContinue');
				if (cmcontinue) {
					if (this.continueKeyPattern.test(cmcontinue)) {
						this.cont = cmcontinue;
					} else {
						this.cont = '';
					}
				}
				
				this.queryApi();
				setTimeout( function () { 
					window.location.hash = "#SlideContainer"; 
				}, 2000); // For IE 6
			},
			toggleVisibility: function () {
				$('#GallerySlideInit').toggle().unbind('click').click(GallerySlide.toggleVisibility);
				$('#SlideContainer').slideToggle();
				$('body').css('overflow', 'visible');
				$(document).trigger('slideshow', ['visibility', GallerySlide]); // For external scripts
			},
			
			queryFile: function ( title ) {
				params = {
					action: 'render',
					title: title
				};
 
				$.ajax({
					url: this.indexURL,
					cache: true,
					dataType: 'html',
					data: params,
					type: 'GET',
					success: function (result, status, x) {
						GallerySlide.processDetails(result, title);
					}
				});
			},
			
			processDetails: function ( result, title ) {
				if ('string' !== typeof result) return;
				
				var i,
					dItem,
					dDescription,
					$node,
					parsedDOM = $(result);
					// dTitle = result.match(/File\:(.+\..{3,5})\&amp;/)[1].replace(/_/g, ' '); // superflous? title ever right?
					
				dDescription = $('<div>').append( 
					$('#fileinfotpl_desc', parsedDOM).siblings().eq(0).contents(), 
					$('#fileinfotpl_aut', parsedDOM).siblings().eq(0).contents(),
					$('#fileinfotpl_date', parsedDOM).siblings().eq(0).contents()
				).html();
				if (!dDescription) dDescription = 'Unable to determine description.';
				
				for (i in this.data) {
					dItem = this.data[i];
					if (dItem.title === title) {
						dItem.description = dDescription;
						$node = $('#desc' + i);
						if ($node.length !== 0) $node.html(dDescription);
					}
				}
			},
 
			queryApi: function () {
				$(document).trigger('slideshow', ['beforeQuery', this]); // For external scripts
				
				params = {
					action: 'query',
					rawcontinue: '',
					generator: 'categorymembers',
					gcmtitle: mw.config.get('wgPageName'),
					gcmlimit: Math.floor($('#SlideContainer').width() / 81) + 1,
					gcmtype: 'file',
					gcmdir: this.cmdir,
					prop: 'imageinfo|categories',
					clprop: 'sortkey|hidden',
					cllimit: 500,
					iiprop: 'url|user|size',
					iilimit: 500,
					iiurlwidth: this.maxImageWidth,
					iiurlheight: this.maxImageHeight,
					format: 'json'
				};
 
				if (this.cont) params.gcmcontinue = this.cont;
				if (!this.initial && !this.cont) return;
 
				$.ajax({
					url: this.apiURL,
					cache: false,
					dataType: 'json',
					data: params,
					type: 'POST',
					success: function (result, status, x) {
						GallerySlide.processReturn(result);
					}
				});
 
			},
 
			processReturn: function (result) {
				$(document).trigger('slideshow', ['afterQuery', this]); // For external scripts
				
				var pages = result.query.pages,
					data = [],
					i = 0;
 
				if (result['query-continue']) {
					if ('undefined' !== typeof this.cont) { 
						this.contOld = this.cont;
					}
					this.cont = result['query-continue'].categorymembers.gcmcontinue;
				}
				else this.cont = false;
				
				// Fromatt a number
				var fm = function( iNr ) {
					iNr += '';
					var rx = /(\d+)(\d{3})/;
					while (rx.test(iNr)) {
						iNr = iNr.replace(rx, '$1' + '<span class="digit-separator">&nbsp;</span>' + '$2');
					}
					return iNr;
				};
 
				for (var id in pages) {
					var v = pages[id],
						r = v.imageinfo[v.imageinfo.length - 1],
						rc = v.imageinfo[0],
						n = data[i] = {},
						sortkey = '',
						$cats = $('<div>', { 'class': 'cat-wrap' }),
						$licenses = $('<div>', { 'class': 'license-wrap' }).text(this.licenseLabel);
					
					// Process categories; Extract visible cats, sortkey for current cat, licenses
					processCats:
					for (var c in v.categories) {
						var tCat = v.categories[c];
						if (tCat.title === mw.config.get('wgPageName').replace(/_/g, ' ')) sortkey = 'file' + '|' + tCat.sortkey + '|' + id;
						if ('undefined' === typeof tCat.hidden) {
							$cats.append( $('<a>', { 'class': 'cat-label', href: mw.util.getUrl(tCat.title), target: '_blank' }).text(tCat.title.replace('Category:', '')), ' ' );
						} else {
							for (var recogID in this.licenseRecognization) {
								if (this.licenseRecognization[recogID][0].test(tCat.title)) {
									$licenses.append( $('<span>', { 'class': 'license-label', title: tCat.title.replace('Category:', '') }).html(this.licenseRecognization[recogID][1]), ' ' );
									continue processCats;
								}
							}
						}
					}
 
					n.title = v.title;
					n['link'] = rc.descriptionurl;
					n.slideUrl = rc.thumburl;
					n.width = rc.thumbwidth;
					n.height = rc.thumbheight;
					n.oWidth = fm(r.width);
					n.oHeight = fm(r.height);
					n.oSize = fm(r.size >> 10) + '&nbsp;<abbr title="1 KibiByte= 1024 Bytes">KiB</abbr>';
					n.$user = $('<span>').text('Uploader: ').append(
						$('<a>', { href: mw.util.getUrl(mw.config.get('wgFormattedNamespaces')[2] + ':' + r.user), target: '_blank' }).text(r.user),
						' (',
						$('<a>', { href: mw.util.getUrl(mw.config.get('wgFormattedNamespaces')[3] + ':' + r.user), target: '_blank' }).text('talk'),
						')'
					);
					n.$cats = $cats;
					n.$licenses = $licenses;
					n.contKey = (sortkey || this.contOld);
					i++;
				}
 
				this.passedData = data;
 
				if (this.initial) {
					this.init();
				} else {
					this.initializeThumbs();
				}
				this.initial = false;
			}
		});
 
		// Now initialize the gallery
		$.extend(this, defaults, settings);
 
		return this;
	};
})(jQuery);
 
$(document).ready(function () {
	if ($('.gallery li').length < 2) return; // no need for a gallery with a few images
	mw.loader.using('mediawiki.cookie', function () {
		$('body').append('<div id="SlideContainer"></div>');
		GallerySlide = $('#SlideContainer').galleriffic();
		
		$('#mw-category-media h2').after('<span id="GallerySlideInit">Show Slideshow</span>');
		$('#GallerySlideInit').click(function () {
			GallerySlide.start();
		});
		
		var lastQuery = mw.cookie.get( 'gs' + mw.config.get('wgPageName').replace('Category:', '') );
		if (lastQuery) {
			$('#mw-category-media h2').after($('<span>', { id: 'GallerySlideContinue' }).text('Continue Slideshow').click( function () {
					if (!GallerySlide.cont) GallerySlide.cont = lastQuery;
					GallerySlide.start();
				}
			));
		}
		
		$(document).trigger('slideshow', ['loaded', GallerySlide]); // For external scripts
		
		var autoStart = mw.util.getParamValue('gsAutoStart');
		if (autoStart) {
			if ('1' === autoStart || 'true' === autoStart || 'yes' === autoStart || '-1' === autoStart) {
				$('#GallerySlideInit').click();
			}
		}
	});
});
 
}