User:Tucoxn/common.js

From Wikimedia Commons, the free media repository
Jump to navigation Jump to search
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.
/**
 * Enhaced POTY - convenient voting!
 * [[Help:EnhancedPOTY.js]]
 *
 * Features:
 *  Eligiblity check, assists to cast votes (from gallery and from vote-page) and vote removal (autodetection & saving in local storage or, if not present from contribs)
 *  Gallery-resize and -randomization based on the user name (seeded random)
 *  Statistics: Some tiny charts (someone has to run genStats() regulary and provide a page to save the data in)
 *  MyPOTY- a control center for each user (what (s)he has voted for, eligibility info & why I can vote here or not, language)
 *
 * Thanks to all translators!
 *
 * <nowiki>
 * jshint validation, please
 *
 * @rev 1 (2012-06-01)
 * @author
 *   [[User:Rillke]], 2012
 * @license 
 *   GPL v.3
 */
 
/*global jQuery:false, mediaWiki:false, Geo:false, importStylesheet:false, importScript:false, wpAvailableLanguages:false, GallerySlide:false*/
/*jshint curly:false */
 
(function($, mw, undefined) {
'use strict';
 
if (window.POTY || 'view' !== mw.config.get('wgAction')) return;
 
var poty;
 
// A bunch of helper functions
function isNumber(n) {
	return !isNaN(parseInt(n, 10)) && isFinite(n);
}
function firstItem(o) { for (var i in o) { if (o.hasOwnProperty(i)) { return o[i]; } } }
function firstItemName(o) { for (var i in o) { if (o.hasOwnProperty(i)) { return i; } } }
function isCanvasSupported() { var elem = document.createElement('canvas'); return !!(elem.getContext && elem.getContext('2d'));}
 
 
/********************************
**
** Translation
**
********************************/
mw.messages.set({
	'poty-poty-year':          "POTY $1",
	'poty-poty-full-year':     "Picture of the Year $1",
	'poty-poty-full-commons':  "POTY - Commons Picture of the Year",
	'poty-welcome-banner':     "Welcome $1! Loading POTY $2.",
	'poty-slideshow':          "Slideshow",
	'poty-fullscreen':         "Full screen",
	'poty-fullscreen-close':   "Close full screen",
 
	'poty-report-error-h1':     "POTY-App experienced an error",
	'poty-report-error':        "The data that will be saved and publicly visible if you send the report: Your username, a timestamp, what the App did immediately before and \"$1\"",
	'poty-report-error-send':   "Send report",
	'poty-report-error-reset':  "Reset and Reload",
	'poty-report-error-cancel': "Cancel",
 
	'poty-ineligible-blocked':       "Your account is ineligible because it is blocked on Commons. You were blocked by $1 because $2 with an expiry of $3.",
	'poty-ineligible-nosul':         "Your account is ineligible because it is not attached to SUL.",
	'poty-ineligible-suleditcount':  "Your account is ineligible because you do not have $1 or more edits on any attached SUL account or on Commons.",
	'poty-ineligible-dateeditcount': "Your account is ineligible because you do not have $1 or more edits on any attached SUL account or on Commons before $2.",
	'poty-eligible': "You are eligible to vote because you have $1 edits on $2 before $3!",
 
	'poty-anonymous-no-vote-msg':   "You are currently not logged in. Only registered users can vote in POTY.",
 
	'poty-vote-add':               "Vote",
	'poty-vote-remove':            "Remove vote",
	'poty-vote-stats':             "Statistics",
	'poty-voting-vote':            "Saving your vote",
	'poty-voting-remove-vote':     "Removing your vote",
	'poty-voting-app-error':       "Application ERROR",
	'poty-voting-edit-error':      "Edit ERROR",
	'poty-vote-nothing-to-remove': "Can\'t find your vote",
	'poty-vote-already-there':     "Already voted this image",
	'poty-vote-multiple-possible': "You may vote for more than one picture",
	'poty-vote-single-only':       "You can vote for only ONE picture",
 
 
	'poty-stats-chart-desc':     "Vote count compared to the average vote count per picture: ",
	'poty-stats-votelist':       "Votelist",
	'poty-stats-close-click':    "Click to close",
 
	'poty-my-poty-link':         "My POTY",
	'poty-my-poty-link-tooltip': "All about your participation in POTY $1",
	'poty-my-poty-h1':           "POTY $1 and You",
	'poty-my-poty-app-version':  "POTY-App-Version $1",
 
	'poty-my-poty-language':     "Language",
	'poty-my-poty-eligibility':  "Eligibility",
	'poty-my-poty-votes':        "Votes",
	'poty-my-poty-state':        "POTY - State",
	'poty-my-poty-data':         "Data saved in your browser",
 
	'poty-my-poty-state-RX':     "Round $1 is running.",
	'poty-my-poty-state-novote': "There is no voting at the moment.",
	'poty-my-poty-state-g-RX':   "You are on a gallery made for round $1.",
 
	'poty-my-poty-action-language-saveoncommons': "Save on Commons",
	'poty-my-poty-action-eligibility-recheck':    "Check again",
	'poty-my-poty-action-votes-recheck':          "Update - Requery",
	'poty-my-poty-action-data-remove':            "Remove",
	'poty-my-poty-action-data-remove-warn':       "After removing this data POTY-App won\'t know what you have voted for."
 
});
 
poty = window.POTY = {
	/********************************
	**
	** Configuration
	**
	********************************/
 
	version: '0.4.3.0',
	// The key to be used to store the data in the user's browser
	storageKey: '2012POTY',
	// POTY <year>
	year: 2012,
	// Will be retrieved from the template $('#potyVotingState')
	state: 'R1',
	// Required e.g. for digging the contribs to a specific date
	startDate: '2013-01-16T00:00:00Z',
 
	// Eligibility requirements
	required: {
		minEditCount: 75 + 1,
		editsBefore: '2013-01-01T00:00:00Z',
		editsDaysAgo: 6000,
		registeredBefore: '2013-01-01T00:00:00Z',
		includeDeleted: false
	},
 
	votingFormat: '\n# [[User:%UserName%|%UserName%]]',
	// Everything with null in it will be initialized later
	formattedVote: null,
	voteRegExp: null,
	votingSummaryAdd: '+1 POTY vote - eligible on %wiki% with %edits% edits - Vote through [[%VoteFrom%]] - [[Help:EnhancedPOTY.js|POTY App]]',
	votingSummaryRemove: '-1 POTY removing vote - Vote through [[%VoteFrom%]] - [[Help:EnhancedPOTY.js|POTY App]]',
 
	// These sizes will be offered in the galleries
	availableImageSizes: [{w: 1280, h: 1024}, {w: 1024, h: 768}, {w: 750, h: 750}, {w: 600, h: 500}, {w: 400, h: 400}, {w: 335, h: 250}, {w: 300, h: 300}, {w: 250, h: 250}, {w: 200, h: 200}, {w: 180, h: 180}, {w: 150, h: 150}, {w: 120, h: 120}],
	availableImageSizesWide: [{w: 1920, h: 600}, {w: 1600, h: 350}, {w: 1280, h: 400}, {w: 800, h: 175}, {w: 600, h: 130}, {w: 400, h: 100}, {w: 300, h: 75}, {w: 200, h: 50}, {w: 100, h: 50}],
	wide: false,
 
	username: mw.config.get('wgUserName') || (window.Geo?Geo.IP:'') || 'anonymous',
	userlanguage: mw.config.get('wgUserLanguage'),
	ratelimit: -1 === $.inArray('autoconfirmed', mw.config.get('wgUserGroups')) ? 8 : -1,
	// Whether the current gallery was made for R1 or the Final
	galleryType: /\d{4}\/Final(?:ists)?/.test(mw.config.get('wgPageName')) ? 'R2' : 'R1',
	votingPageRegExp: /^Commons:Picture of the Year\/\d{4}\/(R1|Final(?:ists)?)\/([^\/]{5,})$/,
	galleryRegExp: /^Commons:Picture of the Year\/\d{4}\/(?:Galleries\/([^\/]+)|(Finalists))/,
 
	// Style & CSS
	stylesheet: 'MediaWiki:EnhancedPOTY.css',
	logo: '//upload.wikimedia.org/wikipedia/commons/9/91/POTY_Logo_For_Banner.png',
	// When thumbs are resized to small sizes, text would overflow otherwise
	iconOnlyWidthThreshold: 250,
 
	// Variables for internal use
	translationLoaded: false,
 
	// Everything that does not need DOM-ready
	init: function () {
		// Variable set up
		poty.formattedVote = poty.getFormattedVote();
		poty.voteRegExp = poty.getVoteRegExp();
		poty.availableImageSizes.reverse();
 
		// Enqueue tasks
		poty.addTask('loadTranslation');
		poty.addTask('splash');
		poty.addTask('loadComponents');
		poty.addTask('launch');
		poty.nextTask();
	},
 
	/**************************
	*
	* The next bunch of Helpers
	*
	**************************/
 
	// For cross-browser-compatibility
	borderRadius: function (r) {
		return {
			'-webkit-border-radius': r,
			'-moz-border-radius': r,
			'-o-border-radius': r,
			'-ms-border-radius': r,
			'border-radius': r
		};
	},
 
	/** Helper - i18n and language
		@param {<dummy>} params  The first param is the message-name; as many arguments of type string or number as the message to create requires
		@return {String} the parsed message (HTML special chars should be escaped by parse)
	**/
	getMessage: function (params) {
		var args = Array.prototype.slice.call(arguments, 0);
		args[0] = 'poty-' + args[0];
		args[args.length] = poty.year;
		return mw.message.apply(this, args).parse();
	},
	/** Helper
		@param {String} stTimestamp  MediaWiki-Timestamp of format YYYY-MM-DDThh:mm:ssZ 
		@return {Object} JavaScript Date-Object
	**/
	getDateFromMWDate: function( stTimestamp ) {
		var regex = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})Z$/;
		var m1 = stTimestamp.match(regex);
		return new Date(m1[1], m1[2]-1, m1[3], m1[4], m1[5], m1[6]); // Wer hat sich diesen Unsinn ausgedacht?
	},
	////////////////////////////
	///////// Storage //////////
	////////////////////////////
	saveData: function () {
		var r;
		// Catch jStorage errors. Odd Safari error occured on Mac.
		// something undefined in '_storage.__jstorage_meta.CRC32'
		try {
			var needsTTL = !poty.dataExists();
			r = $.jStorage.set(poty.storageKey, poty.data);
			if (needsTTL) {
				// Expires in 50d
				$.jStorage.setTTL(poty.storageKey, 4320000000);
			}
		} catch (ex) {
			r = ex;
		}
		return r;
	},
	getData: function () {
		var d = $.jStorage.get(poty.storageKey);
		return $.extend(true, d, {
			R1: {
				votes: {}
			},
			R2: {
				votes: {}
			},
			novote: {
				votes: {}
			},
			local: {
			}
		});
	},
	deleteData: function() {
		try {
			return $.jStorage.deleteKey(poty.storageKey);
		} catch (ex) { 
			return ex;
		}
	},
	dataExists: function() {
		return !!$.jStorage.get(poty.storageKey);
	},
	getVotingPage: function (p, r) {
		return 'Commons:Picture of the Year/' + poty.year + '/' + ((r || poty.galleryType) === 'R1' ? 'R1' : 'Finalists') + '/' + p;
	},
	getVotingPageURL: function (p, r) {
		return mw.util.wikiScript('index') + '?' + $.param({ title: poty.getVotingPage(p, r).replace(/ /g, '_') });
	},
	getFormattedVote: function () {
		return poty.votingFormat.replace(/%UserName%/g, poty.username);
	},
	getVoteRegExp: function () {
		return new RegExp(poty.mdEscapeSpecial($.escapeRE(poty.votingFormat.replace(/%UserName%/g, poty.username))), 'g');
	},
	/** Helper-Escape some special characters that $.escapeRE does not
		@param {String} string  String that's special chars should be escaped
		@return {String} Escaped string
	**/
	mdEscapeSpecial: function(string) {
		var specials = ['t', 'n', 'v', '0', 'f'];
		$.each(specials, function(i, s) {
			var rx = new RegExp('\\'+s, 'g');
			string = string.replace(rx, '\\'+s);
		});
		return string;
	},
	getFileNameFromPageName: function(pagename) {
		if ('string' !== typeof pagename) pagename = mw.config.get('wgPageName');
		return pagename.replace(/_/g, ' ').match(poty.votingPageRegExp)[2];
	},
	getGalleryTypeFromPageName: function(pagename) {
		if ('string' !== typeof pagename) pagename = mw.config.get('wgPageName');
		return pagename.replace(/_/g, ' ').match(poty.votingPageRegExp)[1] === 'R1' ? 'R1' : 'R2';
	},
	getGalleryNameFromPageName: function(pagename) {
		if ('string' !== typeof pagename) pagename = mw.config.get('wgPageName');
		var m = pagename.replace(/_/g, ' ').match(poty.galleryRegExp);
		return m[1] || m[2];
	},
 
	/********************************
	**
	** Startup
	**
	********************************/
	loadComponents: function () {
		// TODO: Replace jquery.form with an API call // use [[MediaWiki:Gadget-SettingsManager.js]]
		mw.loader.using(['mediawiki.util', 'ext.gadget.math.seedrandom', 'ext.gadget.libAPI', 'ext.gadget.libUtil', 'ext.gadget.jquery.fullscreen', 'jquery.jStorage', 'jquery.form', 'jquery.spinner', 'jquery.ui', 'jquery.client'], poty.nextTask);
	},
	relaunch: function() {
		poty.tasks = [];
		setTimeout(poty.launch, 200);
	},
	launch: function () {
		poty.data = poty.getData();
 
		// delete test data, to be removed soon
		$.jStorage.deleteKey('2011POTY');
		$.jStorage.deleteKey('2012POTY_test');
 
		poty.dataExist = poty.dataExists();
		poty.wide = !!$('#potyWide').length;
		if (poty.wide) {
			poty.availableImageSizes = poty.availableImageSizesWide.reverse();
		}
 
		var msgToShow = 'poty-full-commons';
		switch ($('#potyVotingState').text()) {
			case 'duringR1': 
				poty.state = 'R1';
				if ('R1' === poty.galleryType) msgToShow = 'vote-multiple-possible';
				break;
			case 'duringR2':
				poty.state = 'R2';
				if ('R2' === poty.galleryType) msgToShow = 'vote-single-only';
				break;
			default:
				poty.state = 'novote';
				break;
		}
		poty.showBannerMessage([msgToShow]);
 
		if (poty.isVotingPage) {
			poty.galleryType = poty.getGalleryTypeFromPageName();
		}
 
		if (mw.user.isAnon()) {
			return poty.showAnonWarning();
		}
 
		poty.addTask('checkLocal');
		poty.addTask('checkLanguage');
 
		if (!poty.data.eligible && !poty.data.ineligible) {
			poty.currentEditGroup = -1;
			poty.queriesRunning = 0;
			poty.addTask('checkSUL');
			poty.addTask('getDbList');
			if (poty.required.includeDeleted) {
				poty.addTask('checkLocalContribs');
				poty.addTask('checkGlobalContribs');
			} else {
				poty.addTask('checkLocalExistingContribs');
				poty.addTask('checkGlobalExistingContribs');
			}
		}
 
		poty.addTask('showEligible');
		poty.nextTask();
	},
 
	/********************************
	**
	** UI: Gallery, styling
	**
	********************************/
	splash: function () {
		$(function() {
			poty.$gallery = $('#potyEasyVoteEnhanced').find('ul.gallery');
			poty.$gallery.$oldParent = poty.$gallery.parent();
 
			var l = poty.$gallery.find('li.gallerybox').length;
			if (l < 2 || l > 250) {
				if ($('#potyEasyVoteEnhancedButton').length){
					poty.isVotingPage = true;
					document.title = poty.getFileNameFromPageName() + ' - ' + poty.getMessage('poty-full-commons');
				} else {
					poty.log('Startup aborted', l);
					return;
				}
			} else {
				document.title = poty.getGalleryNameFromPageName() + ' - ' + poty.getMessage('poty-full-commons');
			}
			mw.loader.using(['ext.gadget.jquery.blockUI', 'jquery.ui'], function () {
				poty.secureCall('showSplash'); 
			});
		});
	},
	showSplash: function () {
		var $loadBanner = poty.$loadBanner = $('<div>').hide().text(poty.getMessage('welcome-banner', poty.username));
		var $loadImage  = poty.$loadImage  = $('<img>', { src: poty.logo });
		var $loadBannerOuter = poty.$loadBannerOuter = $('<div>', { style: 'font-size:2em; padding:0.5em; min-height:1.15em' })
			.append($loadImage, $loadBanner);
 
		poty.secureCall('showLoader');
 
		setTimeout(function () {
			$loadBanner.hide().fadeIn(1000);
		}, 100);
 
		poty.nextTask();
		importStylesheet(poty.stylesheet);	
	},
	showLoader: function () {
		if (poty.loaderShown) return;
		poty.loaderShown = true;
		if (poty.loaderHideTimeout) clearTimeout(poty.loaderHideTimeout);
		if (poty.$loadBanner.parent().hasClass('ui-effects-wrapper')) poty.$loadBanner.parent().css('height', 'auto');
 
		var css = poty.borderRadius('10px');
		$.blockUI({
			message: poty.$loadBannerOuter,
			css: $.extend(css, {
				border: 'none',
				padding: '15px',
				backgroundColor: '#000',
				'-ms-filter': "progid:DXImageTransform.Microsoft.Alpha(Opacity=70)",
				opacity: 0.7,
				color: '#fff'
			})
		});
	},
	hideLoader: function () {
		poty.loaderHideTimeout = setTimeout(function() {
			poty.$loadBanner.stop().clearQueue().removeAttr('style');
			$.unblockUI({ fadeOut: 1500, onUnblock: function() { if (poty.$loadBanner.parent().hasClass('ui-effects-wrapper')) poty.$loadBanner.parent().css('height', 'auto'); } });
			poty.loaderShown = false;
		}, 200);
	},
	showBannerMessage: function (msg) {
		poty.$loadBanner.hide('clip', {}, 1500, function () {
			$(this).text(poty.getMessage.apply(this, msg));
		}).show('clip', {}, 1000).delay(1500);
	},
	showImmediateBannerMessage: function (msg) {
		if (poty.$loadBanner.parent().hasClass('ui-effects-wrapper')) poty.$loadBanner.unwrap(poty.$loadBanner.parent());
		poty.$loadBanner.stop().clearQueue().removeAttr('style').text(poty.getMessage.apply(this, msg)).show();
	},
	mdCreatejIcon: function (iconClass) {
		return $('<span>', { 'class': 'ui-icon ' + iconClass + ' md-inline-icon', text: ' ' });
	},
	createNotifyArea: function(textNode, icon, state) {
		return $('<div>', { 'class': 'ui-widget' }).append(
			$('<div>', { 'class': state + ' ui-corner-all', style: 'margin-top:5px; padding:0.7em;' }).append($('<p>').append(
				this.mdCreatejIcon(icon).css('margin-right', '.3em'), textNode
			))
		);
	},
	showAnonWarning: function () {
		var dlgBtns = {};
		var $logInNode = $('#pt-login');
		dlgBtns[$logInNode.text() || 'LogIn'] = function() {
			var logInLink = ($logInNode.find('a').length ? $logInNode.find('a').attr('href') : $logInNode.attr('href')) || mw.util.wikiScript() + '?' + $.param({ title: 'Special:UserLogin', returnto: mw.config.get('wgPageName') });
			window.location = logInLink;
		};
		$('<div>').text(poty.getMessage('anonymous-no-vote-msg')).dialog({
			title: poty.getMessage('poty-full-commons'),
			buttons: dlgBtns,
			modal: true,
			open: function() {
				// Look out for http://bugs.jqueryui.com/ticket/6830 / jQuery UI 1.9 / fixed in 1.10.0
				var $buttons = $(this).parent().find('.ui-dialog-buttonpane button');
				$buttons.eq(0).button({ icons: { primary: 'ui-icon-key' } }).addClass('ui-button-green');
				poty.hideLoader();
			},
			close: function() {
				poty.hideLoader();
			}
		});
		return;
	},
	setUpGallery: function () {
		var $gallery = poty.$gallery;
		var $imgs = $gallery.find('div.thumb a.image img:first-child');
		// Support for videos
		$imgs = $imgs.add($gallery.find('div.thumb video:first-child')).add($gallery.find('div.thumb img.playerPoster')).add($gallery.find('div.thumb div.PopUpMediaTransform img'));
 
		// Remove styling from gallery (important on panorama pages)
		$gallery.removeAttr('style');
 
		// Current thumb size and set up vars we need (perhaps, if the user wants them) later
		var currentThumbSize = poty.newSize = 5;
		var currentThumbSizeWidth = $imgs.attr('width');
		poty.resizeRunning = false;
 
		$imgs.each(function(i, el) {
			var $el = $(el),
				w = $el.attr('width');
			currentThumbSizeWidth = Math.max(currentThumbSizeWidth, w);
		});
 
		var lastDiff = Infinity;
		$.each(poty.availableImageSizes, function(i, s) {
			var currentDiff = Math.abs(s.w-currentThumbSizeWidth);
			if (currentDiff < lastDiff) {
				poty.newSize = currentThumbSize = i;
				lastDiff = currentDiff;
			} else {
				return false;
			}
		});
 
		poty.images = {};
		poty.arrImgs = [];
		$imgs.each(function(i, el) {
			var $el = $(el),
				h = $el.height(),
				w = $el.width(),
				src = $el.attr('src') || $el.attr('poster'),
				fileName = mw.libs.commons.titleFromImgSrc(src),
				imgInfo = poty.images[fileName] = { el: $el, sizes: {}, name: fileName };
			imgInfo.sizes[currentThumbSize] = { url: src, h: h, w: w };
			poty.arrImgs.push('File:' + fileName);
			$el.data('potyInfo', imgInfo);
			$el.parents('li.gallerybox').addClass('poty-element').css('width', currentThumbSizeWidth+25).children('div').css('width', currentThumbSizeWidth+25).children('div.thumb').css('width', currentThumbSizeWidth+20);
		});
 
		// Resize-slider and input
		poty.viewContainer = $('<div>', { id: 'potyViewContainer' });
		var $sizeContainer = $('<div>', { id: 'potyResizeContainer', 'class': 'poty-element' }).appendTo(poty.viewContainer);
		var $sizeInput = $('<input>', { 
			type: 'text', 
			size: 5, 
			id: 'sizeInput', 
			'class': 'numbersOnly', 
			value: poty.availableImageSizes[currentThumbSize].w 
		}).appendTo($sizeContainer)
			.bind('input change keyup', function() {
				// IE hack: Otherwise change does not fire
				var valNew = this.value.replace(/[^0-9]/g,'');
				if (valNew !== this.value) this.value = valNew;
			})
			.keyup(function(e) {
				if (13 === e.keyCode-0) $(this).triggerHandler('change');
			})
			.change(function() {
				var tVal = this.value;
				if (!tVal) return;
				var lastDiff = Infinity;
				var nearestVal = currentThumbSize;
				$.each(poty.availableImageSizes, function(i, s) {
					var thisdiff = Math.abs(s.w - tVal);
					if (lastDiff > thisdiff) {
						nearestVal = i;
					} else {
						return false;
					}
					lastDiff = thisdiff;
				});
				this.value = poty.availableImageSizes[nearestVal].w;
				$sizeSlider.slider('option', 'value', nearestVal);
			});
		var $sizeSlider = $('<div>', { css: { 'display': 'inline-block', width: '250px', margin: '10px' } })
			.slider({ 
				max: poty.availableImageSizes.length-1, 
				animate: true,
				change: function(e, ui) {
					$sizeInput.val( poty.availableImageSizes[ui.value].w );
					poty.secureCall('resizeThumbs', ui.value);
				},
				slide: function(e, ui) {
					$sizeInput.val( poty.availableImageSizes[ui.value].w );
				},
				value: currentThumbSize
			})
			.appendTo($sizeContainer.append(' px<br/>'));
		var $oldSizeContainer = $('span.thumb-size-bar');
		$('<label>', { 'for': 'sizeInput', text: $oldSizeContainer.find('.thumb-size-text').text() + ' ' }).prependTo($sizeContainer);
		$oldSizeContainer.after(poty.viewContainer).hide();
 
		if ($.isNumeric(poty.data.lastThumbSize) && (poty.data.lastThumbSize-0) !== currentThumbSize && !poty.wide) {
			$sizeSlider.slider('option', 'value', poty.data.lastThumbSize);
		} else {
			poty.nextTask();
		}
 
	},
	installSlideshow: function () {
		var $buttons = $('<span>').attr('id', 'potyViewButtons'),
			$fullscreen,
			$slideshow;	
 
		var startSlideshow = function (o, cont, screenread) {
			if (cont) o.cont = cont;
			if (screenread) {
				o.readFromScreen = true;
				o.readFromScreenSmallImages = true;
				o.autoPlay = true;
				o.remoteUse = true;
			}
			o.start();
		};
		var loadSlideshowAndStart = function (cont, screenread) {
			if ('object' === typeof window.GallerySlide) {
				if ($.isFunction(GallerySlide.toggleVisibility)) {
					GallerySlide.toggleVisibility();
				} else {
					startSlideshow(window.GallerySlide, cont, screenread);
				}
			} else {
				$(document).bind('slideshow', function (e, st, o) {
					// If the code requires debugging, you can uncomment the following line
					poty.log('Slideshow> ' + st);
					if ('codeLoaded' === st && o) {
						startSlideshow(o, cont, screenread);
					}
				});
				poty.log('loading Slideshow');
				importScript('MediaWiki:GallerySlideshow.js');
				importStylesheet('MediaWiki:Gadget-GallerySlideshow.css');
			}
		};
 
 
		if ($.FullScreenSupported) {
			var _fullScreen = function(prevent) {
				if (false !== prevent) $(document.documentElement).requestFullScreen();
 
				var $el = $fullscreen;
				$el.unbind('click', _fullScreen).click(_closeFullScreen);
				$el.button({ label: poty.getMessage('fullscreen-close'), icons: { primary: 'ui-icon-newwin' } });
			};
			var _closeFullScreen = function(prevent) {
				if (false !== prevent) $.FullScreen.cancelFullScreen();
 
				var $el = $fullscreen;
				$el.unbind('click', _closeFullScreen).click(_fullScreen);
				$el.button({ label: poty.getMessage('fullscreen'), icons: { primary: 'ui-icon-extlink' } });
			};
			$(document).fullScreenChange(function() {
				if ($.FullScreen.isFullScreen()) {
					_fullScreen(false);
				} else {
					_closeFullScreen(false);
				}
			});
			$fullscreen = $('<button type="button" role="button" id="potyFullscreenButton">').text(poty.getMessage('fullscreen'))
				.button({ icons: { primary: 'ui-icon-extlink' } })
				.click(_fullScreen)
				.appendTo($buttons);
		}
		$slideshow = $('<button type="button" role="button" id="potySlideshowButton">').text(poty.getMessage('slideshow'))
			.button({ icons: { primary: 'ui-icon-play' } })
			.click(function() {
				loadSlideshowAndStart(0, true);
			})
			.appendTo($buttons);
		if ($fullscreen) $buttons.buttonset();
		$buttons.appendTo(poty.viewContainer);
 
		poty.nextTask();
	},
	imgsToQuery: [],
	resizeThumbs: function (newSize) {
		poty.imageSizeInCache = newSize in firstItem(poty.images).sizes;
		poty._resizeThumbs(newSize);
	},
	_resizeThumbs: function (newSize) {
		if (poty.resizeRunning) return;
		poty.resizeRunning = true;
		poty.newSize = newSize;
		if (!poty.wide) poty.data.lastThumbSize = newSize;
		if (poty.imageSizeInCache) {
			return poty.secureCall('resizeThumbsCb');
		}
		if (0 === poty.imgsToQuery.length) poty.imgsToQuery = poty.arrImgs;
		var toQuery = poty.imgsToQuery.slice(0,50);
		poty.imgsToQuery = poty.imgsToQuery.slice(50);
 
		poty.queryAPI({
			action: 'query',
			prop: 'imageinfo',
			iiprop: 'url',
			iiurlwidth: poty.availableImageSizes[newSize].w,
			iiurlheight: poty.availableImageSizes[newSize].h,
			titles: toQuery.join('|'),
			requestid: newSize
		}, 'resizeThumbsCb', undefined, 'POST');
		if (!poty.loaderShown) {
			poty.showImmediateBannerMessage(['poty-year']);
			poty.showLoader();
		}
	},
	resizeThumbsCb: function (r) {
		var newSize = r ? r.requestid : poty.newSize;
		var wasReady = true;
		if (poty.imgsToQuery.length) {
			// If there are images to query in the pipe, 
			// immediately query the remaining ones to speed-up execution
			wasReady = poty.resizeRunning = false;
			poty._resizeThumbs(poty.newSize);
		}
		var doResize = function(img) {
			// Prepare for resizing
			var newFullW = poty.availableImageSizes[newSize].w,
				newFullH = poty.availableImageSizes[newSize].h,
				$galleryBox = img.el.parents('li.gallerybox');
 
			// Resize the gallerybox
			var $vDiv = $galleryBox.css('width', newFullW+25).children('div').css('width', newFullW+25).children('div.thumb').css('width', newFullW+20).children('div');
 
			if (!(newSize in img.sizes)) return;
			var newW = img.sizes[newSize].w;
			var newH = img.sizes[newSize].h;
 
			// Video support
			var $player = $galleryBox.find('div.mwPlayerContainer');			
			if ($player.length) {
				$galleryBox.find('div.mwPlayerContainer').css('width', newW).css('height', newH);
			}
			var $miniPreview = $galleryBox.find('div.PopUpMediaTransform');
			$miniPreview.removeAttr('style');
 
 
			img.el.attr('src', img.sizes[newSize].url);
			img.el.css('width', newW).attr('width', newW);
			img.el.css('height', newH).attr('height', newH);
			$vDiv.css('margin', Math.round(newFullH+20-newH)/2 + 'px auto');
		};
 
		if (r) {
			var pgs = r.query.pages;
			$.each(pgs, function(idx, pg) {
				if (!pg.imageinfo) return;
				var ii = pg.imageinfo[0],
					img = poty.images[pg.title.replace('File:', '')];
 
				// Save result
				img.sizes[newSize] = { url: ii.thumburl, h: ii.thumbheight, w: ii.thumbwidth };
				doResize(img);
			});
		} else if (poty.imageSizeInCache) {
			$.each(poty.images, function(i, img) {
				doResize(img);
			});
		}
		if (wasReady) {
			poty.resizeRunning = false;
			poty.saveData();
			if (poty.availableImageSizes[poty.newSize].w < poty.iconOnlyWidthThreshold) {
				poty.buttons.$text.hide();
			} else {
				poty.buttons.$text.show();
			}
			if (poty.tasks.length) {
				poty.nextTask();
			} else {
				poty.hideLoader();
			}
		}
	},
	setUpButtons: function() {
		var statsLabel = poty.getMessage('vote-stats'),
			$voteButtonText   = $('<span>', { 'class': 'poty-vote-button-text' }),
			$statusButtonText = $('<span>', { 'class': 'poty-vote-button-text', text: statsLabel });
 
		if (poty.isVotingPage) {
			var fileName   = poty.getFileNameFromPageName(),
				label      = poty.data[poty.galleryType].votes[fileName] ? poty.getMessage('vote-remove') : poty.getMessage('vote-add'),
				iconClass  = poty.data[poty.galleryType].votes[fileName] ? 'poty-ui-icon-vote-minus' : 'poty-ui-icon-vote-plus',
				$container = $('#potyEasyVoteEnhancedButton'),
				$vbt = $voteButtonText.clone().text(label);
 
			poty.buttons.$text = poty.buttons.$text.add($vbt);
			$('<button>', { title: label })
				.prepend($vbt)
				.prepend($('<span>', { 'class': iconClass }))
				.button({ disabled: poty.state !== poty.galleryType || !poty.data.eligible }).appendTo($container).data('potyImgName', fileName).click(poty.voteThroughVotingpage);
		} else {
			$.each(poty.images, function(i, img) {
				var label = poty.data[poty.galleryType].votes[i] ? poty.getMessage('vote-remove') : poty.getMessage('vote-add'),
					$galleryBox = img.el.parents('li.gallerybox'),
					$galleryText = $galleryBox.find('div.gallerytext'),
					oldText = $galleryText.text(''),
					$vbt = $voteButtonText.clone().text(label),
					$sbt = $statusButtonText.clone(),
					$votingButton = $('<button>', { css: { 'float': 'left' }, title: label })
						.prepend($vbt)
						.prepend($('<span>', { 'class': poty.data[poty.galleryType].votes[i] ? 'poty-ui-icon-vote-minus' : 'poty-ui-icon-vote-plus' }))
						.button({ disabled: poty.state !== poty.galleryType || !poty.data.eligible }).appendTo($galleryText).data('potyImgName', i).click(poty.voteThroughGallery),
					$statsButton = $('<button>', { css: { 'float': 'right' }, title: statsLabel })
						.prepend($sbt)
						.prepend($('<span>', { 'class': 'poty-ui-icon-vote-stats' }))
						.button().appendTo($galleryText).data('potyImgName', i).click(poty.showStats);
 
				// jQuery UI destroys the reference to $vbt
				poty.buttons.$text = poty.buttons.$text.add($votingButton.find('.poty-vote-button-text')).add($statsButton.find('.poty-vote-button-text'));
			});
			if (poty.availableImageSizes[poty.newSize].w < poty.iconOnlyWidthThreshold) poty.buttons.$text.hide();
		}
		// Enforcing single vote
		if ('R2' === poty.state && 0 !== poty.getVoteCount(poty.data.R2.votes)) {
			$('.poty-ui-icon-vote-plus').parents('button').button('option', 'disabled', true);
		}
		poty.nextTask();
	},
	detachGallery: function() {
		var h = poty.$gallery.height();
		poty.$gallery.$placeholder = $('<div>').text('Doing sophisticated magic!').height(h).appendTo(poty.$gallery.$oldParent);
		poty.$gallery.detach();
		poty.nextTask();
	},
	attachGallery: function() {
		poty.$gallery.$placeholder.remove();
		poty.$gallery.appendTo(poty.$gallery.$oldParent);
		poty.nextTask();
	},
	enqueueGalleryShuffle: function() {
		if (Math.seedrandom) {
			poty.secureCall('shuffleGallery');
		} else {
			$(document).bind('scriptLoaded.POTYListenerR', function(e, st) {
				if ('SeedRandom' === st) {
					$(document).unbind('scriptLoaded.POTYListenerR');
					poty.secureCall('shuffleGallery');
				}
			});
		}
		poty.nextTask();
	},
	// seeded "randomization" based on the user-name
	shuffleGallery: function() {
		// Initialize the generator with the user-name
		Math.seedrandom(poty.username);
 
		// Detaching from DOM to speed-up large gallery-shuffle
		// if we would just know $li.length ...
		poty.$gallery.detach().each(function (i, ul) {
			var $ul = $(ul),
				$li = $ul.children('li.gallerybox');
 
			while ($li.length) {
				$ul.append($li.splice(Math.floor(Math.seededrandom() * $li.length), 1)[0]);
			}
		});
		if (!poty.willAttachLater) poty.$gallery.appendTo(poty.$gallery.$oldParent);
 
		poty.saveData();
	},
	checkLanguage: function() {
		// Only change the language automatically for new users who haven't set their language yet
		var browserLang = navigator.userLanguage || navigator.language || navigator.browserLanguage;
		if (!poty.dataExist && poty.data.local.editcount < 5 && browserLang !== poty.userlanguage && 'en' === poty.userlanguage && browserLang in wpAvailableLanguages) {
			poty.secureCall('changeLangTo', browserLang);
		} else {
			poty.nextTask();
		}
	},
 
	/********************************
	**
	** My POTY
	**
	********************************/
	installMyPOTY: function() {
		var $p = mw.util.addPortletLink('p-personal', '#', poty.getMessage('my-poty-link'), 'pt-poty', poty.getMessage('my-poty-link-tooltip'), '', document.getElementById('pt-logout'));
		if ($p) {
			$p = $($p);
			$p.click(poty.myPOTY);
		}
		poty.nextTask();
	},
	changeLangTo: function(lcId, cb) {
		var _savedPrefs = function() {
			if ($.isFunction(cb)) {
				cb();
			} else {
				poty.userlanguage = lcId;
				poty.secureCall('loadTranslation');
			}
		};
		var _gotPrefs = function(r) {
			var $r = $(r);
			$r.find('#mw-input-wplanguage').val(lcId);
			$r.find('#mw-prefs-form').ajaxSubmit(_savedPrefs);
		};
		$.get(mw.util.getUrl('Special:Preferences'), '', _gotPrefs);
	},
	myPOTY: function(e) {
		if (e) e.preventDefault();
		var $d = $('<div>');
		$('<div>', { css: { 'float': 'right' } }).append($('<img>', { src: poty.logo, css: { width: '80px', height: 'auto' } })).appendTo($d);
		$('<div>', { css: { 'font-size': 'smaller' }, text: poty.getMessage('poty-full-commons') + '; ' + poty.getMessage('my-poty-app-version', poty.version) }).appendTo($d);
		var $l = $('<div>', { id: 'myPotyLang' }).append($('<h2>', { text: poty.getMessage('my-poty-language') })).appendTo($d);
		var $s = $('<div>', { id: 'myPotyState' }).append($('<h2>', { text: poty.getMessage('my-poty-state') })).appendTo($d);
		var $e = $('<div>', { id: 'myPotyEligibility' }).append($('<h2>', { text: poty.getMessage('my-poty-eligibility') })).appendTo($d);
		var $v = $('<div>', { id: 'myPotyVotes' }).append($('<h2>', { text: poty.getMessage('my-poty-votes') })).appendTo($d);
		var $a = $('<div>', { id: 'myPotyData' }).append($('<h2>', { text: poty.getMessage('my-poty-data') })).appendTo($d);
 
		// Language
		var $ls;
		$('<button>', { text: poty.getMessage('my-poty-action-language-saveoncommons'), style: 'float:right;' })
			.button({ icons: { primary: 'ui-icon-disk' }}).click(function() {
				var $btn = $(this);
				$btn.unbind('click');
				var _done = function() {
					$btn.unblock();
					poty.reloadPage();
				};
				$btn.block({ message: $.createSpinner() });
				poty.changeLangTo($ls.val(), _done);
			}).appendTo($l);
 
		$ls = $('<select>', { size: 1 }).appendTo($l);
		$.each(wpAvailableLanguages, function(s, l) {
			$ls.append($('<option>', { 'value': s, text: l }));
		});
		$ls.val(poty.userlanguage);
 
		// State
		var statetext;
		switch (poty.state) {
			case 'R1':
			case 'R2':
				statetext = poty.getMessage('my-poty-state-RX', poty.state.slice(1));
				break;
			case 'novote':
				statetext = poty.getMessage('my-poty-state-novote');
				break;
		}
		$('<p>', { text: statetext + ' ' + poty.getMessage('my-poty-state-g-RX', poty.galleryType.slice(1)) }).appendTo($s);
 
		// Eligibility
		$('<button>', { text: poty.getMessage('my-poty-action-eligibility-recheck'), style: 'float:right;' })
			.button({ icons: { primary: 'ui-icon-search' }}).click(function() {
				delete poty.data.ineligible;
				delete poty.data.eligible;
				poty.saveData();
				poty.reloadPage();
			}).appendTo($e);
		if (poty.data.eligible) {
			$('<p>', { text: poty.getMessage('eligible', poty.data.eligible.edits, poty.data.eligible.on.name, poty.getDateFromMWDate(poty.required.editsBefore).toLocaleString()) }).appendTo($e);
		} else {
			var reason = firstItemName(poty.data.ineligible),
				item = firstItem(poty.data.ineligible),
				args = ['ineligible-' + reason];
			/*jshint onecase:true*/
			switch (reason) {
				case 'blocked': 
					args.push(item.by, item.reason, item.exp);
					break;
				default:
					args.push(poty.required.minEditCount, poty.getDateFromMWDate(poty.required.editsBefore).toLocaleString());
					break;
			}
			$('<p>', { text: poty.getMessage.apply(this, args) }).appendTo($e);
		}
 
		// Votes
		$('<button>', { text: poty.getMessage('my-poty-action-votes-recheck'), style: 'float:right;' })
			.button({ icons: { primary: 'ui-icon-search' }}).click(function() {
				var $btn = $(this);
				$btn.unbind('click');
				$btn.block({ message: $.createSpinner() });
 
				// TODO: Find a cleaner way
				poty.tasks = [];
				poty.addTask(function() {
					$d.dialog('close');
					poty.myPOTY();
				});
				poty.contribsDigger();
			}).appendTo($v);
		var listVotes = function(r) {
			$('<h3>', { text: r }).appendTo($v);
			var $vl = $('<ul>', { css: { 'max-height': '150px', 'overflow': 'auto' } }).appendTo($v);
			$.each(poty.data[r].votes, function(f, bool) {
				if (bool) $('<li>').append($('<a>', { href: mw.util.getUrl('File:' + f), text: f }), ' ', $('<a>', { href: poty.getVotingPageURL(f, r), text: '(vp)' })).appendTo($vl);
			});
		};
		listVotes('R1');
		listVotes('R2');
 
		// Data
		var $dd = $('<div>', { css: { 'max-height': '100px', overflow: 'scroll', background: '#fff', border: '1px dotted gray' }, text: $.toJSON(poty.data) }).appendTo($a);
		poty.createNotifyArea($('<span>', { text: poty.getMessage('my-poty-action-data-remove-warn') }), 'ui-icon-alert', 'ui-state-highlight').appendTo($a);
		$('<button>', { text: poty.getMessage('my-poty-action-data-remove') }).appendTo($a).button({ icons: { primary: 'ui-icon-trash' }}).click(function() {
			poty.deleteData();
			poty.data = poty.getData();
			$dd.text($.toJSON(poty.data));
		});
 
		$d.dialog({
			title: poty.getMessage('my-poty-h1'),
			modal: true,
			height: 'auto',
			width: Math.min($(window).width(), 700),
			close: function() {
				$(this).remove();
			}
		});
	},
	// Contibs digger
	diggerRunning: false,
	digMap: {},
	uccontinue: '',
	contribsDigger: function() {
		if (poty.diggerRunning) poty.log('abort new instance: digger is running');
		poty.diggerRunning = true;
		poty.uccontinue = '',
		poty.data.R1.votes = {};
		poty.data.R2.votes = {};
		poty.secureCall('digContribs');
	},
	digContribs: function() {
		poty.queryAPI({
			action: 'query',
			rawcontinue: '',
			list: 'usercontribs',
			uclimit: 'max',
			ucend: poty.startDate,
			uccontinue: poty.uccontinue,
			ucuser: poty.username,
			ucnamespace: 4,
			ucprop: 'title'
		}, 'digContribsCb', undefined, 'POST');
	},
	digContribsCb: function(r) {
		var uc = r.query.usercontribs;
		if (0 === uc.length) return poty.digVotingPages();
		$.each(uc, function(i, c){
			if (poty.votingPageRegExp.test(c.title)) poty.digMap[c.title] = true;
		});
		if (r['query-continue']) {
			poty.uccontinue = r['query-continue'].usercontribs.ucstart;
			poty.digContribs();
		} else {
			return poty.secureCall('digVotingPages');
		}
	},
	digVotingPages: function() {
		var toQuery = [];
		$.each(poty.digMap, function(p, b) {
			if (b) {
				toQuery.push(p);
				poty.digMap[p] = false;
				if (toQuery > 20) return false;
			}
		});
		if (toQuery.length) {
			poty.queryAPI({
				action: 'query',
				prop: 'revisions',
				rvprop: 'content',
				redirects: true,
				titles: toQuery.join('|')
			}, 'digVotingPagesCb', undefined, 'POST');
		} else {
			poty.diggerRunning = false;
			poty.saveData();
			poty.nextTask();
		}
	},
	digVotingPagesCb: function(r) {
		var pgs = r.query.pages;
		$.each(pgs, function(ids, p) {
			// Page moved, deleted, whatever that should never happen in usercontribs ...
			if (!p.revisions) return;
			var content = p.revisions[0]['*'];
			var title = p.title;
			poty.voteRegExp.lastIndex = 0;
			if (poty.voteRegExp.test(content)) {
				var r, t, m = title.match(poty.votingPageRegExp);
				r = m[1].replace('Finalists', 'R2');
				t = m[2].replace('File:', '');
				poty.data[r].votes[t] = true;
			}
		});
		poty.digVotingPages();
	},
 
	/********************************
	**
	** Statistics
	**
	********************************/
	showStats: function() {
		var $b			= $(this),
			$gb			= $b.parents('li.gallerybox'),
			fileName	= $b.data('potyImgName'),
			$chart		= $('<div>').text('Loading chart');
 
		// Incompatible browsers
		var incompat = {
			'camino': 2 // Blacklisted due to a report by [[User:Kersti Nebelsiek]] on [[Special:Permalink/72839941#Results?]] (was version 1.6 but let's be sure)
		};
		var clnt = $.client.profile();
 
		if ((!isCanvasSupported() && 'msie' !== clnt.name) || ((clnt.name in incompat) && (incompat[clnt.name] >= clnt.versionNumber))) {
			window.location = poty.getVotingPageURL(fileName);
			return;
		}
		var sparklineLoaded = false,
			statsLoaded     = false;
 
		if ($.fn.sparkline) sparklineLoaded = true;
		if (!sparklineLoaded) importScript('User:Rillke/jquery.sparkline.js');
 
		var isReady = function(e, st) {
			if (sparklineLoaded && statsLoaded) poty.secureCall('showStatsCb', $chart, $gb, fileName);
		};
 
		$(document).bind('scriptLoaded.POTYListener', function(e, st) {
			if ('jquery.sparkline' === st) {
				$(document).unbind('scriptLoaded.POTYListener');
				sparklineLoaded = true;
				isReady();
			}
		});
		poty.secureCall('loadStats', function() {
			statsLoaded = true;
			isReady();
		});
 
		var w = $gb.width();
		w = Math.min(w < 350 ? Math.round((400/w)*40) : 30, 90);
		$gb.block({ 
			message: $('<div>', { style: 'font-size:smaller' })
				.text(poty.getMessage('stats-chart-desc'))
				.append($chart)
				.append($('<a>', { href: poty.getVotingPageURL(fileName), text: poty.getMessage('stats-votelist') })),
			css: {
				width: w + '%'
			}
		});
		$gb.find('.blockUI').css({'cursor': 'default'}).attr('title', poty.getMessage('stats-close-click')).click(function() { $gb.unblock(); });
		setTimeout(function() {
			$gb.unblock();
		}, 10000);
	},
	showStatsCb: function($chart, $gb, fileName) {
		var diffAvgVots = [],
			imageVots = [],
			maxVoteCount = 0,
			maxDiff = 0;
 
		imageVots = poty.statistics[fileName];
		$.each(imageVots, function(i, votecount) {
			var totalVotes = 0,
				voteCont = 0;
			$.each(poty.statistics, function(fName, votes) {
				if ('number' === typeof votes[i]) {
					if (votes[i] > maxVoteCount) maxVoteCount = votes[i];
					totalVotes += votes[i];
					voteCont++;
				}
			});
			var diff = votecount-Math.round(totalVotes/voteCont);
			if (Math.abs(diff) > maxDiff) maxDiff = Math.abs(diff);
			diffAvgVots.push(diff);
		});
		// height: diff of max equals 60px
		var h = Math.round(maxDiff*60/maxVoteCount) + 5;
		setTimeout(function() {
			$chart.sparkline(diffAvgVots, { height: h + 'px', width: '80%', type: 'bar', fillColor: false });
		}, 500);
	},
	genStats: function() {
		poty.genericVoteRegExp = new RegExp(poty.mdEscapeSpecial($.escapeRE(poty.votingFormat)).replace(/%UserName%/g, '[^\\|\\[\\]]+'), 'g');
		mw.loader.using(['jquery.json'], function() {
			poty.gapfrom = '';
			poty.secureCall('loadStats', poty.updateStats);
		});
	},
	loadStats: function(cb) {
		if (poty.statistics) return cb();
		$.ajax({
			url: mw.util.wikiScript(),
			dataType: 'script',
			data: {
				title: 'User:Rillke/POTYStats' + poty.year + poty.galleryType + '.js',
				action: 'raw',
				ctype: 'text/javascript',
				// Disallow caching
				maxage: 0,
				smaxage: 0
			},
			cache: false,
			complete: function() {		
				if (!poty.statistics) poty.statistics = {};
				if (!poty.statisticsDates) poty.statisticsDates = [new Date()];
				return cb();
			}
		});
	},
	updateStats: function() {
		poty.queryAPI({
			action: 'query',
			rawcontinue: '',
			generator: 'allpages',
			gapnamespace: 4,
			gapfilterredir: 'nonredirects',
			gaplimit: 100,
			gapprefix: 'Picture of the Year/' + poty.year + '/' + (poty.galleryType === 'R1' ? 'R1' : 'Finalists') + '/',
			gapfrom: poty.gapfrom,
			prop: 'revisions',
			rvprop: 'content'
		}, 'updateStatsCB', undefined, 'POST');
	},
	updateStatsCB: function(r) {
		var pgs = r.query.pages;
		$.each(pgs, function(ids, pg) {
			var c = pg.revisions[0]['*'],
				t = pg.title,
				f = poty.getFileNameFromPageName(t),
				m = c.match(poty.genericVoteRegExp),
				l = 0;
			if (m) l = m.length;
			if (!(f in poty.statistics)) poty.statistics[f] = [0];
			poty.statistics[f].push(l);
		});
		if (!r['query-continue']) {
			poty.statisticsDates.push(new Date());
			mw.libs.commons.api.editPage({
				editType: 'text',
				title: 'User:Rillke/POTYStats' + poty.year + poty.galleryType + '.js',
				text: 'POTY.statistics=' + $.toJSON(poty.statistics) + ';\n\n' +
					'POTY.statisticsDates=' + $.toJSON(poty.statisticsDates) + ';\n\n' +
					"$(document).triggerHandler(\'statsLoaded\', [\'POTYStats" + poty.year + "\', POTY.statistics]);",
				summary: 'updating voting statistics',
				recreate: false,
				minor: true,
				watchlist: 'nochange'
			});
		} else {
			poty.gapfrom = r['query-continue'].allpages.gapcontinue;
			poty.secureCall('updateStats');
		}
	},
 
	/********************************
	**
	** Voting
	**
	********************************/
	voteR2Listener: function() {
		poty.nextTask();
 
		// Will be executed when there is time to
		var __handleStorageChange = function() {
			var oldVotecount = poty.getVoteCount(poty.data.R2.votes);
			$.jStorage.reInit();
			poty.data = poty.getData();
			var newVoteCount = poty.getVoteCount(poty.data.R2.votes);
			if (0 === newVoteCount) {
				var $minusButtons = $('.poty-ui-icon-vote-minus');
				var addMsg = poty.getMessage('vote-add');
				$minusButtons.parents('button').attr('title', addMsg).find('.poty-vote-button-text').text(addMsg);
				$minusButtons.removeClass('poty-ui-icon-vote-minus').addClass('poty-ui-icon-vote-plus');
				$('.poty-ui-icon-vote-plus').parents('button').button('option', 'disabled', false);
			} else {
				$('.poty-ui-icon-vote-minus, .poty-ui-icon-vote-plus').parents('button').button('option', 'disabled', true);
				// Reload page only if user removed vote (prevents reload-loops between tabs)
				if (oldVotecount !== newVoteCount) poty.reloadPage();
			}
		};
		var t = 0;
		$.jStorage.listenKeyChange(poty.storageKey, function(e) {
			clearTimeout(t);
			t = setTimeout(__handleStorageChange, 750);
		});
	},
	voteBlockImg: function($el, msg) {
		if ('R1' === poty.galleryType) {
			var w = $el.width();
			w = Math.min(w < 350 ? Math.round((400/w)*40) : 30, 90);
			$el.block({ 
				message: poty.getMessage(msg),
				css: {
					width: w + '%'
				}
			});
		} else {
			poty.secureCall('showLoader');
			poty.secureCall('showImmediateBannerMessage', [msg]);
		}
	},
	voteUnblockImg: function($el) {
		if ('R1' === poty.galleryType) {
			setTimeout(function () {
				$el.unblock();
			}, 1000);
		} else {
			poty.secureCall('hideLoader');
		}
	},
	voteMessageAndUnblock: function($el, msg) {
		if ('R1' === poty.galleryType) {
			poty.voteBlockImg($el, msg);
			setTimeout(function () {
				poty.voteUnblockImg($el);
			}, 2000);
		} else {
			setTimeout(function () {
				poty.secureCall('hideLoader');
			}, 2000);
			poty.secureCall('showImmediateBannerMessage', [msg]);
		}
	},
	voteSetButtonPlus: function($b) {
		$b.find('.poty-ui-icon-vote-minus').removeClass('poty-ui-icon-vote-minus').addClass('poty-ui-icon-vote-plus');
		var addMsg = poty.getMessage('vote-add');
		$b.attr('title', addMsg).find('.poty-vote-button-text').text(addMsg);
	},
	voteSetButtonMinus: function($b) {
		$b.find('.poty-ui-icon-vote-plus').removeClass('poty-ui-icon-vote-plus').addClass('poty-ui-icon-vote-minus');
		var rmMsg = poty.getMessage('vote-remove');
		$b.attr('title', rmMsg).find('.poty-vote-button-text').text(rmMsg);
	},
	voteThroughGallery: function() {
		var $b          = $(this),
			fileName    = $b.data('potyImgName'),
			$img        = poty.images[fileName].el,
			$galleryBox = $img.parents('li.gallerybox');
		poty.secureCall('vote', fileName, $galleryBox, $b);
	},
	voteThroughVotingpage: function() {
		var $b = $(this),
			fileName = poty.getFileNameFromPageName();
		poty.secureCall('vote', fileName, $b.parent(), $b, poty.reloadPage);
	},
	getVoteCount: function(votelist) {
		var c = 0;
		$.each(votelist, function(i, b) {
			if (b) c++;
		});
		return c;
	},
	vote: function(fileName, $toBlock, $b, readyCb) {
		var votingPage     = poty.getVotingPage(fileName),
			addText        = poty.formattedVote,
			removeRegExp   = poty.voteRegExp,
			basetimestamp  = '',
			starttimestamp = '',
			wikitext       = '',
			remove         = poty.data[poty.galleryType].votes[fileName];
 
		if (remove) {
			poty.voteBlockImg($toBlock, 'voting-remove-vote');
		} else {
			poty.voteBlockImg($toBlock, 'voting-vote');
		}
		var _goToVotingPage = function() {
			window.location = poty.getVotingPageURL(fileName);
		};
		var __addVoteOk = function(r) {
			poty.voteSetButtonMinus($b);
			if ('R1' === poty.state && 0 === poty.getVoteCount(poty.data.R1.votes)) {
				poty.voteMessageAndUnblock($toBlock, 'vote-multiple-possible');
			} else if ('R2' === poty.state) {
				// Enforcing single vote
				poty.voteMessageAndUnblock($toBlock, 'vote-single-only');
				$('.poty-ui-icon-vote-plus').parents('button').button('option', 'disabled', true);
			} else {
				poty.voteUnblockImg($toBlock);
			}
			poty.data[poty.galleryType].votes[fileName] = true;
			if ('undefined' !== typeof r.edit['new']) {
				poty.autoreport('++created ' + votingPage);
			}
			poty.saveData();
			if ($.isFunction(readyCb)) readyCb();
		};
		var __addVoteErr = function(t, r, q) {
			poty.voteMessageAndUnblock($toBlock, 'voting-edit-error');
			poty.fail('voteadd ' + votingPage + '; ' + t, _goToVotingPage);
		};
		var _addVote = function() {
			mw.libs.commons.api.editPage({
				cb: __addVoteOk,
				errCb: __addVoteErr,
				editType: 'appendtext',
				title: votingPage,
				text: addText,
				summary: poty.votingSummaryAdd
					.replace('%wiki%', poty.data.eligible.on.name)
					.replace('%edits%', poty.data.eligible.edits)
					.replace('%VoteFrom%', mw.config.get('wgPageName').replace(/_/g, ' ')),
				recreate: false,
				minor: true,
				redirect: true,
				watchlist: 'nochange'
			});
		};
		var __removeVoteOk = function(r) {
			poty.voteSetButtonPlus($b);
			poty.voteUnblockImg($toBlock);
			poty.data[poty.galleryType].votes[fileName] = false;
			poty.saveData();
			if ('R2' === poty.state && 0 === poty.getVoteCount(poty.data.R2.votes)) {
				// Enforcing single vote
				$('.poty-ui-icon-vote-plus').parents('button').button('option', 'disabled', false);
			}
			if ($.isFunction(readyCb)) readyCb();
		};
		var __removeVoteErr = function(t, r, q) {
			poty.voteMessageAndUnblock($toBlock, 'voting-edit-error');
			poty.fail('voteremove ' + votingPage + '; ' + t, _goToVotingPage);
		};
		var _removeVote = function() {
			var newText = wikitext.replace(removeRegExp, '');
			mw.libs.commons.api.editPage({
				cb: __removeVoteOk,
				errCb: __removeVoteErr,
				editType: 'text',
				title: votingPage,
				text: newText,
				summary: poty.votingSummaryRemove
					.replace('%VoteFrom%', mw.config.get('wgPageName').replace(/_/g, ' ')),
				recreate: false,
				minor: true,
				redirect: true,
				watchlist: 'nochange'
			});
		};
		var _gotWikitext = function(r) {
			try {
				var p = firstItem(r.query.pages),
					rv;
 
				if (p.revisions) {
					rv = p.revisions[0];
					starttimestamp = p.starttimestamp;
					basetimestamp = r.timestamp;	
					wikitext = rv['*'];
				} else {
					wikitext = '';
				}
 
				// Cave:
				// http://stackoverflow.com/questions/1520800/why-regexp-with-global-flag-in-javascript-give-wrong-results
				removeRegExp.lastIndex = 0;
				var contains = removeRegExp.test(wikitext);
 
				if (remove) {
					if (contains) {
						_removeVote();
					} else {
						poty.voteMessageAndUnblock($toBlock, 'vote-nothing-to-remove');
						poty.voteSetButtonPlus($b);
						poty.data[poty.galleryType].votes[fileName] = false;
					}
				} else {
					if (contains) {
						poty.voteMessageAndUnblock($toBlock, 'vote-already-there');
						poty.voteSetButtonMinus($b);
						poty.data[poty.galleryType].votes[fileName] = true;
					} else {
						_addVote();
					}
				}
			} catch (ex) {
				poty.voteMessageAndUnblock($toBlock, 'voting-app-error');
				poty.fail(ex + ' at ' + votingPage, _goToVotingPage);
			}
			poty.saveData();
		};
		poty.queryAPI({
			action: 'query',
			prop: 'info|revisions',
			intoken: 'edit',
			rvprop: 'timestamp|content',
			rvlimit: 1,
			titles: votingPage,
			redirects: 1
		}, _gotWikitext, null, 'POST');
 
		poty.saveData();
	},
 
 
	/********************************
	**
	** Eligiblity check
	**
	********************************/
	showIneligible: function () {
		poty.saveData();
		poty.log('ineligible');
 
		poty.secureCall('continueEligible');
	},
	showEligible: function () {
		poty.saveData();
		poty.log('Eligible!');
 
		poty.secureCall('continueEligible');
	},
	continueEligible: function () {
		// Just task scheduling
		poty.tasks = [];
		poty.buttons = {
			$text: $()
		};
 
		if (!poty.isVotingPage) {
			poty.addTask('detachGallery');
			poty.addTask('setUpGallery');
		}
		poty.addTask('installMyPOTY');
		if (!poty.isVotingPage) poty.addTask('installSlideshow');
		if (!poty.dataExist && poty.data.local.editcount > 0) poty.addTask('contribsDigger');
		poty.addTask('setUpButtons');
		if ('R2' === poty.state && 'R2' === poty.galleryType) poty.addTask('voteR2Listener');
		if (!poty.isVotingPage) {
			poty.addTask('enqueueGalleryShuffle');
			poty.willAttachLater = true;
			poty.addTask('attachGallery');
		}
		poty.addTask('hideLoader');
		poty.nextTask();
	},
	checkLocal: function () {
		poty.queryAPI({
			action: 'query',
			meta: 'userinfo',
			uiprop: 'blockinfo|ratelimits|editcount|registrationdate|preferencestoken'
		}, 'checkLocalCb');
	},
	checkLocalCb: function (r) {
		poty.log('userinfo', r);
		var ui = r.query.userinfo;
		poty.username = ui.name;
		if (ui.ratelimits.ip) {
			var l = ui.ratelimits.edit.ip;
			if (l) {
				poty.ratelimit = Math.floor(l.hits / (l.seconds / 60));
			}
		}
		if (ui.blockexpiry) {
			poty.data.ineligible = {
				blocked: {
					by: ui.blockedby,
					reason: ui.blockreason,
					exp: ui.blockexpiry
				}
			};
			return poty.secureCall('showIneligible');
		} else {
			if (poty.data.ineligible && poty.data.ineligible.blocked) poty.data.ineligible = null;
		}
		if (poty.data.local.editcount && poty.data.local.id !== ui.id) {
			poty.log('Wrong user data. Ereasing...');
			poty.deleteData();
			poty.data = {};
			poty.saveData();
			return poty.secureCall('relaunch');
		}
		poty.data.local = {
			id: ui.id,
			editcount: ui.editcount,
			registrationdate: ui.registrationdate
		};
		poty.preferencestoken = ui.preferencestoken;
		poty.nextTask();
		if ('string' === typeof ui.anon && !mw.user.isAnon()) {
			return poty.showAnonWarning();
		}
	},
	checkSUL: function () {
		poty.queryAPI({
			action: 'query',
			meta: 'globaluserinfo',
			guiprop: 'merged',
			guiuser: poty.username
		}, 'checkSULCb');
	},
	checkSULCb: function (r) {
		var sul = r.query.globaluserinfo,
			sulmap = {},
			sularr = [];
 
		if (typeof sul.missing !== 'undefined') {
			poty.data.sulmissing = true;
			return poty.nextTask();
		}
		poty.data.sul = {
			creationTime: sul.registration,
			id: sul.id
		};
		$.each(sul.merged, function (i, account) {
			// Map the number of edits to wikis and also add the edit-numbers to an array
			if (account.wiki === 'commonswiki') return;
			if (!sulmap[account.editcount]) sulmap[account.editcount] = [];
			sulmap[account.editcount].push(account);
			sularr.push(account.editcount - 0);
		});
		var numsort = function (n1, n2) {
				return n1 - n2;
			};
		sularr.sort(numsort);
		poty.sulInfo = [];
		var seenEditCount = -1;
		var i = sularr.length - 1;
		for (; i !== -1; i--) {
			var editcount = sularr[i];
			if (editcount === seenEditCount) continue;
			if (editcount < poty.required.minEditCount) continue;
			poty.sulInfo.push(sulmap[editcount]);
			seenEditCount = editcount;
		}
		if (0 === poty.sulInfo.length && poty.data.local.editcount < poty.required.minEditCount) {
			poty.data.ineligible = {
				suleditcount: true
			};
			return poty.secureCall('showIneligible');
		}
		poty.nextTask();
	},
	getDbList: function () {
		poty.queryAPI({
			action: 'sitematrix'
		}, 'getDbListCb');
	},
	getDbListCb: function (r) {
		poty.sitematrix = {};
		$.each(r.sitematrix, function (i, sites) {
			if (!isNumber(i)) return;
			$.each(sites.site, function (x, s) {
				poty.sitematrix[s.dbname] = {
					api: s.url.replace('http://', '//') + '/w/api.php',
					langcode: sites.code,
					typecode: s.code
				};
			});
		});
		$.each(r.sitematrix.specials, function (i, s) {
			poty.sitematrix[s.dbname] = {
				api: s.url.replace('http://', document.location.protocol + '//') + '/w/api.php',
				specialcode: s.code
			};
		});
		poty.nextTask();
	},
	checkLocalExistingContribs: function () {
		poty.queryAPI({
			action: 'query',
			list: 'usercontribs',
			ucuser: poty.username,
			ucstart: poty.required.editsBefore,
			uclimit: poty.required.minEditCount,
			ucprop: '',
			requestid: 'commonswiki'
		}, 'checkLocalExistingContribsCb');
	},
	checkLocalExistingContribsCb: function (r) {
		var uc = r.query.usercontribs;
		if (uc.length < poty.required.minEditCount) {
			// Not enough edits
			return poty.nextTask();
		}
		poty.data.eligible = {
			on: {
				name: r.requestid,
				details: poty.sitematrix[r.requestid]
			},
			edits: uc.length + '+'
		};
		poty.showEligible();
	},
	checkLocalContribs: function () {
		poty.queryAPI({
			action: 'userdailycontribs',
			user: poty.username,
			daysago: poty.required.editsDaysAgo,
			basetimestamp: poty.required.editsBefore,
			requestid: 'commonswiki'
		}, 'checkLocalContribsCb');
	},
	checkLocalContribsCb: function (r) {
		var uc = r.userdailycontribs;
 
		// First check the registration date
		if (parseInt(uc.registration, 10) > parseInt(poty.required.registeredBefore.replace(/\D/g, ''), 10)) {
			// Registered later
			return poty.nextTask();
		}
		if (parseInt(uc.timeFrameEdits, 10) < poty.required.minEditCount) {
			// Not enough edits
			return poty.nextTask();
		}
		poty.data.eligible = {
			on: {
				name: r.requestid,
				details: poty.sitematrix[r.requestid]
			},
			edits: uc.timeFrameEdits,
			registration: uc.registration
		};
		poty.showEligible();
	},
	checkGlobalExistingContribs: function () {
		if (poty.data.sulmissing) {
			poty.data.ineligible = {
				nosul: true
			};
			return poty.secureCall('showIneligible');
		}
		// Next group of contribs-count
		poty.currentEditGroup++;
		if (!poty.sulInfo[poty.currentEditGroup]) {
			poty.data.ineligible = {
				dateeditcount: true
			};
			return poty.secureCall('showIneligible');
		}
		$.each(poty.sulInfo[poty.currentEditGroup], function (i, s) {
			if (!(s.wiki in poty.sitematrix)) throw new Error('There are contributions in CentralAuth for an unknown wiki.');
			var url = poty.sitematrix[s.wiki].api;
			poty.queryAPI({
				action: 'query',
				list: 'usercontribs',
				ucuser: poty.username,
				ucstart: poty.required.editsBefore,
				uclimit: poty.required.minEditCount,
				ucprop: '',
				requestid: s.wiki
			}, 'checkGlobalExistingContribsCb', url);
			poty.queriesRunning++;
		});
	},
	checkGlobalExistingContribsCb: function (r) {
		poty.queriesRunning--;
		var uc = r.query.usercontribs;
		if (poty.data.eligible) return;
		if (uc.length < poty.required.minEditCount) {
			// Not enough edits
			if (0 === poty.queriesRunning) poty.secureCall('checkGlobalExistingContribs');
			return;
		}
		poty.data.eligible = {
			on: {
				name: r.requestid,
				details: poty.sitematrix[r.requestid]
			},
			edits: uc.length + '+'
		};
		poty.showEligible();
	},
	checkGlobalContribs: function () {
		if (poty.data.sulmissing) {
			poty.data.ineligible = {
				nosul: true
			};
			return poty.secureCall('showIneligible');
		}
		// Next group of contribs-count
		poty.currentEditGroup++;
		if (!poty.sulInfo[poty.currentEditGroup]) {
			poty.data.ineligible = {
				dateeditcount: true
			};
			return poty.secureCall('showIneligible');
		}
		$.each(poty.sulInfo[poty.currentEditGroup], function (i, s) {
			if (!(s.wiki in poty.sitematrix)) throw new Error('There are contributions in CentralAuth for an unknown wiki.');
			var url = poty.sitematrix[s.wiki].api;
			poty.queryAPI({
				action: 'userdailycontribs',
				user: poty.username,
				daysago: poty.required.editsDaysAgo,
				basetimestamp: poty.required.editsBefore,
				requestid: s.wiki
			}, 'checkGlobalContribsCb', url);
			poty.queriesRunning++;
		});
	},
	checkGlobalContribsCb: function (r) {
		poty.queriesRunning--;
		var uc = r.userdailycontribs;
		if (poty.data.eligible) return;
		// First check the registration date
		if (parseInt(uc.registration, 10) > parseInt(poty.required.registeredBefore.replace(/\D/g, ''), 10)) {
			// Registered later
			if (0 === poty.queriesRunning) poty.secureCall('checkGlobalContribs');
			return;
		}
		if (parseInt(uc.timeFrameEdits, 10) < poty.required.minEditCount) {
			// Not enough edits
			if (0 === poty.queriesRunning) poty.secureCall('checkGlobalContribs');
			return;
		}
		poty.data.eligible = {
			on: {
				name: r.requestid,
				details: poty.sitematrix[r.requestid]
			},
			edits: uc.timeFrameEdits,
			registration: uc.registration
		};
		poty.showEligible();
	},
	loadTranslation: function() {
		var l = poty.userlanguage;
		switch (l) {
			case 'nb':
				l = 'no';
				break;
			case 'zh-hans':
			case 'zh-cn':
			case 'zh-sg':
			case 'zh-my':
				l = 'zh-hans';
				break;
			default:
				l = l.split('-')[0];
		}
		if (poty.userlanguage !== 'en') {
			poty.translationLoaded = true;
			$.ajax({
				url: mw.util.wikiScript(),
				dataType: 'script',
				data: {
					title: 'MediaWiki:EnhancedPOTY.js/' + l + '.js',
					action: 'raw',
					ctype: 'text/javascript',
					// Allow caching for 1/2 day
					maxage: 43200,
					smaxage: 43200
				},
				cache: true,
				success: poty.nextTask,
				error: poty.nextTask
			});
		} else {
			poty.nextTask();
		}
	},
 
	/**
	** Does a MediaWiki API request and passes the result to the supplied callback.
	**/
	queryAPI: function (params, callback, url, method) {
		mw.libs.commons.api.query(params,
		{
			cache: false,
			url: url,
			method: method,
			cb: function(r) {
				poty.secureCall(callback, r);
			},
			// r-result, query, text
			errCb: function(t, r, q) {
				poty.fail(t);
			}
		});
	},
 
	fail: function (err, cb) {
		poty.log('error', err);
		if (typeof err === 'object') {
			var stErr = err.message + ' \n\n ' + err.name;
			if (err.lineNumber) stErr += ' @line' + err.lineNumber;
			err = stErr;
		}
		var $dlg = $('<div>'),
			dlgBtns = {};
 
		if (((poty.data.eligible && !poty.data.eligible.on) || -1 !== err.indexOf(' \'on\'')) && $.toJSON) err = err + ' e:' + $.toJSON(poty.data.eligible) + ';\n i:' + $.toJSON(poty.data.ineligible);
		dlgBtns[poty.getMessage('report-error-send')] = function() {
			$dlg.parent().block();
			poty.autoreport(err, function() {
				$dlg.dialog('close');
				$dlg.remove();
				if ($.isFunction(cb)) cb();
			});
		};
		dlgBtns[poty.getMessage('report-error-reset')] = function() {
			// Delete saved data as this could be a problem
			poty.deleteData();
			// Purge the page and reloadPage
			poty.purgePage();
			$dlg.parent().block();
		};
		dlgBtns[poty.getMessage('report-error-cancel')] = function() {
			$dlg.dialog('close');
		};
		try {
			poty.hideLoader();
		} catch (ex) {}
		poty.createNotifyArea($('<span>', { text: poty.getMessage('report-error', err) }), 'ui-icon-alert', 'ui-state-error').appendTo($dlg);
		$dlg.dialog({
			title: poty.getMessage('report-error-h1'),
			buttons: dlgBtns,
			modal: true,
			height: 'auto',
			width: Math.min($(window).width(), 500),
			open: function() {
				// Look out for http://bugs.jqueryui.com/ticket/6830 / jQuery UI 1.9
				var $buttons = $(this).parent().find('.ui-dialog-buttonpane button');
				$buttons.eq(0).button({ icons: { primary: 'ui-icon-circle-check' } });
				$buttons.eq(1).button({ icons: { primary: 'ui-icon-wrench' } });
				$buttons.eq(2).button({ icons: { primary: 'ui-icon-circle-close' } });
			},
			close: function() {
				poty.hideLoader();
			}
		});
	},
 
	autoreport: function (errText, cb) {
		var randomId = Math.round(Math.random()*1099511627776);
		var currentTask = $.isFunction(poty.currentTask) ? (poty.currentTask.name ? poty.currentTask.name : 'inline') : poty.currentTask;
		var toSend = '\n== Autoreport by POTY ' + randomId + ' ==\n' + errText + 
			'\n++++\n:Task: ' + currentTask + '\n:NextTask: ' + poty.tasks[0] + '\n:LastTask: ' + poty.tasks[poty.tasks.length - 1] +
			'\n:Page: ' + (mw.config.get('wgPageName')) + '\n:Skin: ' + mw.user.options.get('skin') +
			'\n:[{{fullurl:Special:Contributions|target={{subst:urlencode:{{subst:REVISIONUSER}}}}&offset={{subst:REVISIONTIMESTAMP}}}} Contribs before error]';
		mw.libs.commons.api.editPage({
			cb: cb,
			errCb: cb,
			editType: 'appendtext',
			title: 'MediaWiki talk:EnhancedPOTY.js/auto-reports',
			text: toSend,
			summary: '[[#Autoreport by POTY ' + randomId + '|Reporting a POTY-App error.]] Random ID=' + randomId,
			minor: true,
			watchlist: 'nochange'
		});
	},
 
	/**
	** Method to catch errors and report where they occurred
	**/
	secureCall: function (fn) {
		var o = poty;
		try {
			o.currentTask = arguments[0];
			if ($.isFunction(fn)) {
				if (fn.name) poty.log(fn);
				return fn.apply(o, Array.prototype.slice.call(arguments, 1)); // arguments is not of type array so we can't just write arguments.slice
			} else if ('string' === typeof fn) {
				poty.log(fn);
				return o[fn].apply(o, Array.prototype.slice.call(arguments, 1)); // arguments is not of type array so we can't just write arguments.slice
			} else {
				poty.log('This is not a function!');
			}
		} catch (ex) {
			poty.log('failure at ' + fn);
			o.fail(ex);
		}
	},
 
	/**
	** Simple task queue.  addTask() adds a new task to the queue, nextTask() executes
	** the next scheduled task.  Tasks are specified as method names to call.
	**/
	tasks: [],
	// list of pending tasks
	currentTask: '',
	// current task, for error reporting
	oldTask: '',
	// task called before
	addTask: function (task) {
		poty.tasks.push(task);
	},
	nextTask: function () {
		var task = poty.currentTask = poty.tasks.shift();
		return ($.isArray(task) ? poty.secureCall.apply(poty, task) : poty.secureCall(task)); // Ja da guckste ...
	},
	lastTask: function () {
		var task = poty.currentTask = poty.tasks[poty.tasks.length - 1];
		poty.tasks = [];
		return ($.isArray(task) ? poty.secureCall.apply(poty, task) : poty.secureCall(task));
	},
	log: function (key, val) {
		if (window.console && $.isFunction(window.console.log)) window.console.log('POTY> ' + key, val/*, this*/);
	},
	reloadPage: function () {
		window.location = window.location;
	},
	purgePage: function (page) {
		window.location = mw.util.wikiScript('index') + '?' + $.param({ title: mw.config.get('wgPageName') || page.replace(/ /g, '_'), action: 'purge' });
	}
};
 
poty.secureCall('init');
})(jQuery, mediaWiki);
// </nowiki>