MediaWiki:JSONListUploads.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.
/**
* JSONListUploads (formaly User:Rillke/JSONListUploads.js)
*
* To load from user scripts:
* if ( mw.config.get('wgPageName').indexOf(mw.user.getName()) !== -1 ) importScript( 'MediaWiki:JSONListUploads.js' )
* (c) 2011 – 2015 by Rainer Rillke; special thanks to Krinkle who had a look at the code.
* DoApiCall and the task-queue are based on a bright idea by DieBuche / Ilmari Karonen
* <nowiki>
* TODO: Settings URL-params like RTRC (or better hash-links) and defaults by user's <skin>.js || common.js – generating a Twinkle-like settings-wizard
* TODO: Switch to mediawiki.Api when it is more usable (e.g. contains a predefined edit token)
* TODO: Slideshow option <-> Make MediaWiki:GallerySlideshow.js more modular or write a bridge
*/
/** JSONListUploads -dependencies; jshint validation **/
/* global mw:false, JSONListUploadsShowModel:false, jQuery:false, Geo:false*/
/* jshint curly: false, scripturl:true, bitwise:false, laxbreak:true, laxcomma:true, smarttabs:true, boss:true, multistr:true, devel:true */

// google-image-search-like popup-boxes
(function ($) {
'use strict';

$.fn.ibox = function (options) {
	if (typeof options !== 'object') options = {};

	var img = this,
		elX = 0,
		elY = 0,
		elW = 0,
		ibox = $('#ibox');
	if (!ibox.length) {
		ibox = $('<div>', { id: 'ibox' });
		options.node.append(ibox);
	}
	var handle = function () {
			var el = $(this);
			ibox.html((typeof options.callBack === 'function') ? options.callBack(el, this) : '');
			elX = el.position().left - 5;
			elY = el.position().top - 5; // 5 = CSS#ibox padding+border
			elW = el.width();

			if (elW < 175) {
				elX = elX - ((175 - elW) >> 1);
				elW = 175;
			}

			var thisclone = $(this).clone();
			thisclone.css('text-align', 'center');
			if (typeof options.rmClass === 'string') thisclone.removeClass(options.rmClass);
			thisclone.prependTo(ibox);
			ibox.css({
				top: elY,
				left: elX,
				width: elW
			});

			ibox.stop(true).fadeTo(400, 1);
		},
		fadeTimeout = 0;

	img.on('mouseenter', handle);

	ibox.on('mouseenter', function () {
		clearTimeout(fadeTimeout);
		ibox.stop(true).fadeTo(400, 1);
	});
	var onMouseleave = function () {
		fadeTimeout = setTimeout(function () {
			ibox.stop(true).fadeTo(400, 0, function () {
				$(this).html('');
				if (typeof options.close === 'function') options.close();
			});
		}, 150);
	};
	ibox.on('mouseleave', onMouseleave);
	if (Array.isArray(options.closeOnLeaveOf)) onMouseleave();
};

// /////////////////////////////////////////////////
/** MAIN CODE **/

if (window.JSONListUploads) return;

var JLU,
	/* TRANSLATION-SUBPAGES */
	i18nData = { de: 1, ru: 1, zh: 1 },
	cfg = mw.config.get([
		'wgCanonicalSpecialPageName',
		'wgFormattedNamespaces',
		'wgPageName',
		'wgScript',
		'wgUserGroups',
		'wgUserLanguage',
		'wgRelevantUserName',
		'wgServer'
	]),
	scriptUrl = cfg.wgServer + cfg.wgScript,
	cThumb = '//upload.wikimedia.org/wikipedia/commons/thumb/',
	$doc = $(document);

JLU = window.JSONListUploads = {

	/* REVISION CONTROL */
	// (x.major.minor.bug)
	// When maintaining this script always bump this number!
	mdScriptRevision: '0.1.18.1',

	/**
	* Runs before document ready and before translation is available
	* (important event-binders should be attached as fast as possible)
	*/
	preinstall: function () {
		var i18nLang = cfg.wgUserLanguage;
		// for the drop-down
		this.userCache = {};
		this.catCache = {};

		// Allow users to do "crappy things"
		$doc.triggerHandler('scriptLoaded', ['JSONListUploads', JLU]);

		mw.util.addCSS(
			'#ibox{position:absolute;overflow-y:none;background:#fff;z-index:1001;display:none;padding:4px;-webkit-box-shadow:0 0 7px 2px #aaa;-moz-box-shadow:0 0 7px 2px #aaa;box-shadow:0 0 7px 2px #aaa;-webkit-border-radius:10px;-moz-border-radius:10px;border-radius:10px;border:1px solid #bbb}' +
			'.accordion {margin-top:2px;min-height:1.6em;line-height:1.7em;}\
			.accordion-content {border:1px solid #CCC;border-top:0 none #FFF;background:#EEE;padding-left:2px;padding-right:2px}\
			.filter-frame {padding:0.5em;line-height:2.6em;border:#CCC solid 1px;white-space:nowrap;}\
			.controlpaneltextbox {height:1.2em;margin-bottom:0.5em;margin-top:0.5em}\
			.controlpanelcheckbox {height:1em;width:1em;margin-bottom:0.7em;margin-top:0.7em}\
			.controlpanelselect {height:1.6em;margin-bottom:0.5em;margin-top:0.5em}\
			.tipsyrevision {font-variant:small-caps;border-bottom:1px dotted #888;margin-right:4px;display:inline-block;cursor:help;}\
			.tipsycategory {border-bottom:1px dotted #888;font-style:italic;margin-right:4px;display:inline-block;cursor:help;}\
			.tipsymetadata {color:#68C;border-bottom:1px dotted #57B;white-space:nowrap;font-style:italic;font-weight:bold;margin-right:4px;display:inline-block;cursor:help;}\
			.j-cat-wrap {font-size:0.8em;margin:1px;line-height:2em;display:block;}\
			a.j-cat-label {border:#AAA solid 1px;padding:2px;background:#000;border-radius:4px;white-space:nowrap;;color:#EEE;}\
			li.j-cat-label {display:inline-block;list-style-image:none;margin-right:2px;overflow:hidden;max-width:100%;text-overflow:ellipsis;color:#EEE;}\
			a.j-cat-label:visited {color:#59F;}\
			li.j-cat-label:hover {max-width:300px}\
			.jFileTime {color:gray;font-size:0.9em;line-height:1.1em;}\
			.jFileSize {color:green;font-size:0.9em;}\
			.jFileMime {color:green;font-size:0.9em;font-style:italic;}\
			.jFileTitle {display:block;text-align:center;padding-right:0 !important;background:#EEE !important;margin-top:3px;border-radius:5px;}\
			.jPureTitle {text-align:center;margin-bottom:5px;font-weight:bold;display:inline-block;}\
			div.jImage {position:relative}\
			div.jGU {position:absolute;right:0;bottom:0;height:0;width:0;overflow:visible;cursor:pointer}\
			.thumb > div > a.image {display:block;background:none !important;padding:0 !important;}\
			.com-mg-pagination {background-color:#EEE;border:1px solid #BBB;margin:3px;padding:2px;line-height:2em;display:inline-block;}\
			.com-mg-pagination-list {display:inline-block;}\n'
		);

		if (i18nLang !== 'en') {
			i18nLang = i18nLang.split('-')[0];
			if (!(i18nLang in i18nData)) {
				i18nLang = 'en';
				// Try better fallback
				var chain = mw.language.getFallbackLanguages();
				for (var i = chain.length - 1; i >= 0; i--) {
					if (chain[i] in i18nData)
						i18nLang = chain[i]; // no break as lower i is better
				}
			}
			if (i18nLang !== 'en') {
				$.ajax({
					url: mw.util.wikiScript(),
					dataType: 'script',
					data: {
						title: this.mdSelfPath + '/' + i18nLang + '.js',
						action: 'raw',
						ctype: 'text/javascript',
						maxage: 2419200 // Allow caching for 28 days
					},
					cache: true,
					success: function () { JLU.install(); },
					error: function () { JLU.install(); }
				});
			}
		}
		if (i18nLang === 'en') this.install();

	},

	/**
	* Set up the JSONListUploads object and add the toolbox link. Called via $doc.ready() during page loading.
	*/
	install: function () {
		var userMatch;

		this.mdHelpLink = $('<a>', { href: mw.util.getUrl('Commons talk:Gallery tool'), target: '_blank' }).append($('<img>', { title: this.i18n.faq, alt: '?', src: '//upload.wikimedia.org/wikipedia/commons/4/45/GeoGebra_icon_help.png', width: '16', height: '16' }));
		this.mdHelpDesk = $('<a>', { href: mw.util.getUrl('COM:HD'), target: '_blank' }).append($('<img>', { title: this.i18n.help, alt: '?', src: cThumb + 'b/b6/Gnome_User_Speech.svg/18px-Gnome_User_Speech.svg.png', width: '18', height: '18' }));
		this.mdExport = $('<a>', { href: 'javascript:JSONListUploads.secureCall("exportList");' }).append($('<img>', { title: this.i18n.export, alt: 'E', src: '//upload.wikimedia.org/wikipedia/commons/d/df/Farm-Fresh_table_export.png', width: '19', height: '19' }));

		if (/^(?:Special|Commons):MyGallery$/.test(cfg.wgPageName))
			this.secureCall('listUploads', { user: mw.util.getParamValue('gUser') });
		else if ((userMatch = cfg.wgPageName.match(/^(?:Special|Commons):MyGallery\/([^\/]+)/)))
			this.secureCall('listUploads', { user: userMatch[1] });
		else if (cfg.wgCanonicalSpecialPageName === 'Listfiles')
			$('#bodyContent').prepend($('<a>', { style: 'float:right', href: 'javascript:JSONListUploads.secureCall("listUploads", { user: "' + (cfg.wgRelevantUserName || $('#mw-listfiles-user > input').val()).replace('\'', '\\\'') + '" });' }).text('Gallery-Tool'));

		// else mw.util.addPortletLink('p-tb', 'javascript:JSONListUploads.secureCall(\'listUploads\');', 'Gallery-Tool', 't-JSONListUploads', null, document.getElementById('tb-uploads'));
		$doc.on('JSONListUploads', function (e, st, user) {
			if (st === 'start')	JLU.secureCall('listUploads', { user: user });
		});
		$doc.triggerHandler('scriptReady', ['JSONListUploads', JLU]);
	},

	/**
	*  Strange, Commons has only very poor christmas files
	*  feel free to add some good ones
	*/
	christmas: function () {
		try {
			var xmasFiles = [
				'Silent Night (choral).ogg',
				'Joy To The World.ogg',
				'U.S. Army Band - Lo How a Rose.ogg',
				'Corelli - Concerto Grosso in G minor - Christmas Concerto - part 2.ogg'];
			this.$gNotify.prepend($('<img>', { src: cThumb + 'a/ab/SMirC-xmas.svg/24px-SMirC-xmas.svg.png', title: 'Ho hoh ho' }), ' ', $('<a>', { href: mw.util.getUrl('File:' + xmasFiles[Math.round(Math.random() * (xmasFiles.length - 1))]), target: '_blank', text: 'Merry christmas!' }), ' ');
		} catch (ex) { } // silently fail
	},

	/**
	*  Making our scripts more popular.
	*  Lots of users simply don't know that we have gadgets
	*/
	wikiNotifyer: function ($node) {
		var o = this,
			username = (mw.user.getName() || Geo.IP),
			// Either display a big welcome or something that may be of interest.
			createHiddenSpan = function (content) {
				return $('<span>').css('display', 'none').append(content);
			},
			createAnchorByPagename = function (wlink, text) {
				return $('<a>', { href: mw.util.getUrl(wlink), target: '_blank' }).text(text);
			},
			createAnchorByUrl = function (url, text) {
				return $('<a>', { href: url, target: '_blank' }).text(text);
			},
			createNfNodeMd = function (text) {
				return createAnchorByPagename('Help:VisualFileChange.js', text);
			},
			createNfNodeCatALot = function (text) {
				return createAnchorByPagename('Help:Gadget-Cat-a-lot', text);
			},
			createNfNodeGadgets = function (text) {
				return createAnchorByUrl(mw.util.getUrl('Special:Preferences') + '#mw-prefsection-gadgets', text);
			},
			createNfNodeRTRC = function (text) {
				return createAnchorByPagename('User:Krinkle/RTRC', text);
			},
			createNfNodeCleanUp = function (text) {
				return createAnchorByPagename('User:Magog the Ogre/cleanup.js', text);
			},
			createNfNodeGlamorous = function (text) {
				return createAnchorByUrl('https://glamtools.toolforge.org/glamorous.php?' + encodeURI(o.user) + '&use_globalusage=1', text);
			},
			createNfNodeMagnus = function (text) {
				return createAnchorByUrl('https://magnustools.toolforge.org/index.html', text);
			},
			createNfNodeFileList = function (text) {
				return createAnchorByPagename('Special:ListFiles/' + o.user, text);
			},
			createNfNodeWeNeedYourHelp = function (text) {
				return createAnchorByPagename('Commons:Welcome', text);
			},
			createNfNodeScripters = function (text) {
				return createAnchorByPagename('Commons:User Scripts', text);
			},
			createNfNodeReview = function (text) {
				return createAnchorByPagename('User talk:Rillke/LicenseReview.js', text);
			},
			createNfNodeUploadStats = function (text) {
				return createAnchorByPagename('User:UploadStatsBot', text);
			},
			createNfNodeSHA1Lookup = function (text) {
				return createAnchorByPagename('COM:SHA1', text);
			},

			nfid = [], // Notifier-Item to Display
			nfsi = [], // Notifier-Source Items
			dDisplayCount = mw.cookie.get('JSONListUploadsVisitCount');
		if (dDisplayCount === null) {
			dDisplayCount = 1;
			mw.cookie.set('JSONListUploadsVisitCount', dDisplayCount, { expires: 200 }); // 200 days
		} else {
			mw.cookie.set('JSONListUploadsVisitCount', ++dDisplayCount, { expires: 200 });
		}

		if (dDisplayCount < 2) {
			nfsi = [
				'Hello ' + username + ', ',
				'welcome to Gallery-Tool. Rillke, formerly supported by Rd232, wrote this tool without fee in his spare-time. I hope it will be helpful to you. ',
				'If you need help, please have a look at:<br>',
				o.mdHelpLink.clone().css('display', 'inline'),
				$('<a>', { href: mw.util.getUrl(o.mdErrReportPath), target: '_blank' }).text(' The documentation and FAQ-page.').append('<br>'),
				o.mdHelpDesk.clone().css('display', 'inline'),
				$('<a>', { href: mw.util.getUrl('COM:HD'), target: '_blank' }).text(' Or ask a question of general nature at helpdesk.').append('<br>'),
				o.mdExport.clone().css('display', 'inline'),
				$('<a>', { href: 'javascript:JSONListUploads.secureCall(\'exportList\');' }).text(' You can export a list of all loaded items that match the filter.').append('<br>'),
				$('<a>', { href: mw.util.getUrl('Special:MyUploads'), target: '_blank' }).text('If something does not work correctly, you can use this list.')
			];
			$.each(nfsi, function (id, nfsii) {
				nfid.push(createHiddenSpan(nfsii));
			});
		} else {
			var wgUG = cfg.wgUserGroups.join(' ');
			if (wgUG.indexOf('sysop') !== -1 || wgUG.indexOf('patroller') !== -1) {
				nfsi.push(
					createNfNodeRTRC('Vandals, beware!'),
					createNfNodeCleanUp('Automatically clean-up file a description-page!'),
					createNfNodeGadgets('There are so may useful gadgets!'),
					createNfNodeMd('Ever tried VisualFileChange to batch-change content?'),
					createNfNodeMd('Want to perform mass-changes on files in a category?'),
					createNfNodeMd('Trusted and reliable. VisualFileChange.'),
					createNfNodeMd('Do. Did. Done. Using VisualFileChange.'),
					createNfNodeMd('RegExp meets mass changes. VisualFileChange.'),
					createNfNodeMd('Want to do mass-changes on the uploads of ' + o.user + '?'),
					createNfNodeSHA1Lookup('Want to know whether a file has old revision dupes?'),
					createNfNodeSHA1Lookup('SHA1 Lookup. At Wikimedia Commons.')
				);
				if (wgUG.indexOf('sysop') !== -1) {
					nfsi.push(
						createNfNodeSHA1Lookup('Wondering whether you ever deleted a specific file?'),
						createNfNodeSHA1Lookup('Find deleted files by SHA1.'),
						createNfNodeSHA1Lookup('Unveil the mystery! Look up whether a certain file has ever been on Commons!')
					);
				}
			}
			if (wgUG.indexOf('otrs-member') !== -1) {
				nfsi.push(
					createNfNodeMd('Easier: Insert permission temlates en-mass with VisualFileChange with auto-clean-up!'),
					createNfNodeMd('Permission letters with 50 files? Use VisualFileChange.'),
					createNfNodeMd('Yeah! Urgh! Got permission for 50 files? VisualFileChange.'),
					createNfNodeMd('OTRS permission for 10, 20 … 100 files? VisualFileChange.')
				);
			}
			if (wgUG.indexOf('Image-reviewer') !== -1 || wgUG.indexOf('sysop') !== -1) {
				nfsi.push(
					createNfNodeReview('Hi Image-reviewer, still using an outdated FlickrReview script? Upgrade now!'),
					createNfNodeReview('Awkward situation if you accidentally reviewed an upload of a black-listed Flickr-user?')
				);
			}
			nfsi.push(
				createNfNodeGadgets('Ever had a look at our collection of gadgets?'),
				createNfNodeGadgets('Looking for a slideshow?'),
				createNfNodeGadgets('Know more. Do more. -While looking at a gallery with "Gallery Details".'),
				createNfNodeGlamorous('Global usage. On toollabs.'),
				createNfNodeUploadStats('Upload statistics on your user page wanted? UploadStatsBot.'),
				createNfNodeMagnus('Useful: Magnus\' toys\'n\'tools.'),
				createNfNodeCatALot('One category → other cat? Cat-a-lot!'),
				createNfNodeCatALot('Username changed? Want your user category to become renamed? Cat-a-lot!'),
				createNfNodeWeNeedYourHelp('Commons needs your help, ' + username + '! Check out what you can do.'),
				createNfNodeScripters('Want to create a cool slideshow framework? Join our project User Scripts!'),
				createNfNodeScripters('Hi, ' + username + ', want creating tools like this? Join our project User Scripts!')
			);

			nfid.push(createNfNodeFileList(o.i18n.filelist).hide());
			// choose a random message
			nfid.push(nfsi[Math.round(Math.random() * (nfsi.length - 1))].hide());
			nfid.push(o.mdHelpDesk.clone().hide());
			nfid.push(o.mdHelpLink.clone().hide());
			nfid.push(o.mdExport.clone().hide());
			$node.css('text-align', 'right');
		}
		var currentDelay = 200,
			curId = 0;
		$.each(nfid, function (id, nfidi) {
			$node.append(nfidi, ' ');
			setTimeout(function () {
				nfid[curId].fadeIn().css('display', 'inline');
				curId++;
			}, currentDelay);
			currentDelay += 400;
		});
	},

	createNextHref: function (gravity) {
		return $('<span>', { name: 'gGetMoreWrap', style: 'display:inline;' }).append(
			$('<sup>').append(this.helpHover.clone().attr('title', this.i18n.gPagesPanel).tipsy({ gravity: gravity })),
			$('<a>', { name: 'gGetMore', href: 'javascript:JSONListUploads.secureCall(\'nextPage\');', title: 'Click here to load the next page.' }).append(mw.html.escape('>> ')).css('display', 'inline')).hide();
	},

	nextPage: function () {
		if (this.gContainerCurrent + 1 === this.gContainers.length) {
		// Last page - load new files
			this.secureCall('nextQuery');
		} else {
		// cached
			this.secureCall('goToPage', this.gContainerCurrent + 1);
		}
	},

	nextQuery: function () {
		this.$gOptions.triggerHandler('lockUI');
		this.gContainers[this.gContainerCurrent].hide();
		this.$outerContainer.prepend(this.secureCall('genGContainer'));
		this.secureCall('createPage');
	},

	goToPage: function (pg) {
		this.gContainers[this.gContainerCurrent].hide();
		this.gContainerCurrent = pg;
		this.gContainers[pg].fadeIn();
		this.secureCall('replacePanels');
		if ((this.gContainers.length - 1) === pg) {
			if (!this.openChachedFiles && this.noMoreFiles) {
				this.$gNextHref.hide();
				this.$gNextHref2.hide();
			}
		} else {
			this.$gNextHref.show().css('display', 'inline');
			this.$gNextHref2.show().css('display', 'inline');
		}
	},

	replacePanels: function () {
		var $tmpPanels = $('[name="gPagesPanel"]'),
			$tmpPanelsNew1 = this.secureCall('createPagesPanel', 30),
			$tmpPanelsNew2 = this.secureCall('createPagesPanel', 25);
		$tmpPanels.eq(0).replaceWith($tmpPanelsNew1);
		$tmpPanels.eq(1).replaceWith($tmpPanelsNew2);
	},

	/* Creats the pages-control-panel */
	createPagesPanel: function (spotsAvailable) {
		var $gPagesPanel = $('<nav>', { 'title': this.i18n.gPagesPanel, 'class': 'com-mg-pagination', 'name': 'gPagesPanel' }).append(this.i18n.page),
			$gPageList = $('<div>', { 'class': 'com-mg-pagination-list' }).appendTo($gPagesPanel),

			containerCount = this.gContainers.length,
			currentContainer = this.gContainerCurrent,
			spotsRequired = 5,
			spotRate = containerCount > spotsAvailable ? Math.ceil(((containerCount - 2) * 2) / (spotsAvailable - spotsRequired) / 5) * 5 : 1,
			prevInserted = -1,
			isInsertionPosition = function (i) {
				return i % spotRate === 0 || i < 2 || i > containerCount - 3 || Math.abs(currentContainer - i) < 3;
			};

		for (var i = 0; i < containerCount; i++) {
			if (currentContainer === i) {
				$gPageList.append($('<b>').append('&nbsp;' + (i + 1) + '&nbsp;'));
				prevInserted = i;
			} else if (isInsertionPosition(i) || isInsertionPosition(i + 1) && isInsertionPosition(i - 1)) {
				if (i - prevInserted > 1)
					$gPageList.append($('<i>').append('&nbsp;…&nbsp;'));

				$gPageList.append($('<a>', { href: 'javascript:JSONListUploads.secureCall(\'goToPage\', ' + i + ');' }).append('&nbsp;' + (i + 1) + '&nbsp;'));
				prevInserted = i;
			}
		}
		$gPagesPanel.append('/' + this.getPagesCount());
		$gPagesPanel.$gPageList = $gPageList;
		return $gPagesPanel;
	},

	getPagesCount: function () {
		var pgc;
		if (!this.openChachedFiles && this.noMoreFiles) {
			return this.gContainers.length;
		} else if (this.haveAllAndNewLoaded) {
			pgc = this.totalMatchingFiles / 24;
			return ((Math.floor(this.totalMatchingFiles / 24) === pgc) ? pgc : (Math.floor(this.totalMatchingFiles / 24) + 1));
		} else if (this.noMoreFiles) {
			pgc = this.openChachedFiles / 24;
			return (this.gContainers.length + ((Math.floor(this.openChachedFiles / 24) === pgc) ? pgc : (Math.floor(this.openChachedFiles / 24) + 1)));
		} else {
			return '?';
		}
	},

	/* A container is a page. It contains all the thumbs. */
	genGContainer: function () {
		this.$gContainer = $('<ul>', { 'class': 'gallery', 'id': ('galleryContainer' + this.gContainers.length) }).prepend(
			this.$gNoResults = $('<div>', { 'class': 'gNoResult', 'style': 'min-height:38px;background:url(//upload.wikimedia.org/wikipedia/commons/3/3b/Farm-Fresh_image_delete.png) #CCC no-repeat 7px 3px;border-radius:10px;' }).append($('<span>', { style: 'position:relative; left:40px' }).append('No filter-results.')).hide()
		);
		if (this.viewMode == '1')
			this.$gContainer.css('text-align', 'center');

		this.gContainers.push(this.$gContainer);
		this.gContainerCurrent++;
		return this.$gContainer;
	},

	/* Called when filter or view-mode changes. Deletes all containers and creates a new one. */
	filterChanged: function () {
		if (!this.validUser) return;
		this.$gOptions.triggerHandler('lockUI');
		this.shownStats = false;
		if (this.noMoreFiles) this.haveAllAndNewLoaded = true;
		this.openChachedFiles = 0;
		this.totalMatchingFiles = 0;
		for (var id in this.uploadsRevs) {
			var gItem = this.uploadsRevs[id],
				mapItem = this.uploadMap[gItem.title];

			mapItem.gbClones = [];
			gItem.gbInUse = false;
			if ((gItem.ii || this.filterByDate) || gItem.del || gItem.delRev) { // Don't process if this was not queried yet
				gItem.matchesFilter = this.secureCall('matchesFilter', gItem);
				if (gItem.matchesFilter) this.openChachedFiles++;
			}
		}
		this.totalMatchingFiles = this.openChachedFiles;
		$.each(this.gContainers, function (cnr, cnri) {
			cnri.remove();
		});
		this.gContainerCurrent = -1;
		this.gContainers = [];
		this.$outerContainer.prepend(this.secureCall('genGContainer'));
		if (this.viewMode == '1') {
			this.$bottomSpacer.show();
			this.$outerContainer.css('margin', '27px');
		} else {
			this.$bottomSpacer.hide();
			this.$outerContainer.css('margin', '0px');
		}
		this.secureCall('createPage');
	},

	/* Does file @param upload matches the filter settings?  */
	matchesFilter: function (upload) {
		if (!upload.le) { // Upload not by user or got no logevent, yet
			return false;
		}
		if (upload.le.action === 'upload' && !this.$gInitial[0].checked) return false;
		if (upload.le.action === 'overwrite') {
			if (upload.revert) {
				upload.le.action = 'revert';
				if (!this.$gRevert[0].checked) return false;
			} else if (!this.$gOverwrite[0].checked) { return false; }
		}
		if (upload.le.action === 'revert' && !this.$gRevert[0].checked) return false;
		if ((upload.del || upload.delRev) && !this.$gDelete[0].checked) return false;
		var gTopVal = this.$gTop.val();
		if ((gTopVal == '1') && (upload.delRev || upload.del)) return false;
		if (typeof upload.ii === 'undefined') return true; // quit here - no cats of a deleted file!
		if (!upload.isLastUserRev && this.$gHideRvs[0].checked) return false;
		if (gTopVal != '0') {
			if ((typeof upload.isOnTop === 'undefined') && (gTopVal == '1')) return false;
			if (upload.isOnTop && (gTopVal == '2')) return false;
		}
		var gCatVal = this.$gCat.val();
		if (upload.cats && gCatVal !== '' && this.i18n.inCatPlaceholder !== gCatVal) {
			var fMatch = false;
			$.each(upload.cats, function (i, cati) {
				var thiscat = cati.title;
				if ('Category:' + gCatVal === thiscat) {
					fMatch = true;
					return false;
				}
			});
			if (!fMatch) return false;
		}
		return true;
	},

	/* Re-Calls listUploads, which sets up a new UI */
	recallMe: function () {
	// First check whether the username is valid
		if (this.validUser === false) return;

		// Then abort pendig API calls
		$.each(this.pendingXHRs, function (id, x) {
			if (x && x.abort) x.abort();
		});
		this.secureCall('listUploads', { user: this.$gUser.val(), oldestFirst: this.oldestFirst, lestart: this.$gStartAt.val() });
	},

	/* Status label on the right bottom-corner */
	setStatus: function (stat) {
		if (stat) {
			this.$gStatusBar.text(stat);
			this.$gStatusBar.fadeIn();
		} else {
			this.$gStatusBar.fadeOut();
		}
	},

	/* Drop-Down Event-Callback */
	seekUsers: function (request, pCallback, byTimeout) {
		var o = this;
		if (byTimeout) o.userACReqsTimeOuts--;
		// Prevent sending queries before other returned, especially on keystrokes
		if (o.userACReqs !== 0 && !o.userACReqsTimeOuts && typeof pCallback !== 'function') {
		// Set up a new timeout request and pCallback are the latest parameters passed to this function
			o.userACReqsTimeOuts++;
			setTimeout(function () { o.seekUsers(request, pCallback, true); }, 100);
			return;
		} else if (o.userACReqs !== 0 && o.userACReqsTimeOuts !== 0 && typeof pCallback !== 'function') {
		// There are other timeouts, just return
			return;
		}
		o.userACReqs++;

		var query = {
			action: 'query',
			list: 'allusers',
			auprefix: request.term.replace(/^(?:User|Benutzer):/, '')
		};
		o.doGetApiCall(query, 'seekUsersCB', pCallback);
	},

	/* Drop-Down API-Callback */
	seekUsersCB: function (result, pCallback) {
		var searchTerms = [];
		this.userACReqs--;

		$.each(result, function (id, useri) {
			searchTerms.push({ id: useri.userid, value: useri.name });
		});
		if (typeof pCallback === 'function') pCallback(searchTerms);
		this.secureCall('doesUserExist');
	},

	doesUserExist: function () {
		var curUsVal = this.$gUser.val(),
			found = false,
			// Normalizing
			normVal = (curUsVal.slice(0, 1).toUpperCase() + curUsVal.slice(1)).replace(/_/g, ' ');
		if (normVal !== curUsVal) {
		// FIXME: Calling val() resets the cursor position.
		// To avoid doing this too much, only call val() when the value changed.
		// This avoids breaking CTRL-A and other keyboard events that don't
		// change the value.
			this.$gUser.val((curUsVal.slice(0, 1).toUpperCase() + curUsVal.slice(1)).replace(/_/g, ' '));
		}
		curUsVal = this.$gUser.val();
		if (curUsVal === this.user) { // current user
			this.validUser = true;
			this.$gUserB.prop('disabled', false);
			return true;
		}
		if (curUsVal in this.userCache) {
			var uterm = this.userCache[curUsVal],
				_this = this;

			$.each(uterm, function (us, utermi) {
				if (curUsVal === utermi.name) {
					_this.validUser = true;
					_this.$gUserB.prop('disabled', false);
					found = true;
					return false;
				}
			});
		}
		if (found) return true;
		this.validUser = false;
		this.$gUserB.prop('disabled', true);
		return false;
	},

	/* Drop-Down Event-Callback */
	seekCats: function (request, pCallback) {
		if (/\^|\>|</.test(request.term)) {
			pCallback([{ value: 'invalid category name' }]);
			return;
		}
		var query = {
			action: 'query',
			list: 'allcategories',
			acprefix: request.term.replace(/^(?:Category|Kategorie):/, '').replace(/_/g, ' ')
		};
		this.doGetApiCall(query, 'seekCatsCB', pCallback);
	},

	/* Drop-Down API-Callback */
	seekCatsCB: function (result, pCallback) {
		var searchTerms = [];
		$.each(result, function (id, cati) {
			searchTerms.push({ value: cati['*'] });
		});
		if (typeof pCallback === 'function') pCallback(searchTerms);
	},

	/** Sets up the UI and prepares the variables
	@param {Object} params  They will overwrite settings set with URL-params, or set in user's <skin>.js || common.js
**/
	listUploads: function (params) {
	// Declarations
		var o = this;
		// ////////
		o.oldestFirst = false;
		if (mw.user && mw.user.getName) o.user = mw.user.getName();
		o.lestart = '';
		o.viewMode = 0;
		var mwDateRx = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/;

		if (typeof params === 'object') {
			if (params.oldestFirst)
				o.oldestFirst = params.oldestFirst;

			if (params.user)
				o.user = params.user.replace(/^User:/, '');

			if (params.lestart) {
				if (mwDateRx.test(params.lestart)) {
					o.lestart = params.lestart;
					o.filterByDate = true;
				}
			}
			if (params.viewMode)
				o.viewMode = params.viewMode;

		}
		if (o.user) o.user = (o.user.slice(0, 1).toUpperCase() + o.user.slice(1)).replace(/_/g, ' '); // Normalizing

		o.uploadsRevs = {}; // revisions
		o.uploadMap = {}; // name --> revisions
		o.uploadMapPureRev = {}; // name --> only image revisions; good for sorting
		o.uploadMoves = {}; // new revision --> old revision
		o.openUploads = []; // files to query information about
		o.qGlobalUsage = {}; // file name --> $DOMNode; files to query globalUsage about
		o.uploadsToProcess = []; // for uploads already queried in detail
		o.uploadsToTrace = []; // for uploads which may be moved or delted
		o.pendingXHRs = []; // XHRs pending - should be canceled before reloading
		o.$inputs = []; // declared later
		o.validUser = true; // prevent loading of non-existing users
		o.iUploads = 0; // count of uploads logged
		o.listUploadsPending = 0; // log-queries
		o.traceUploadsPending = 0; // trace-queries
		o.openChachedFiles = 0; // used to determine the pages-count and important controller
		o.totalMatchingFiles = 0; // after filtering - for calculating the pages-count
		o.haveAllAndNewLoaded = false;
		o.noMoreFiles = false; // lestart was not in response
		o.shownStats = false; // statistics shown?
		o.doingSilentWork = false; // For our background job
		o.UIWaiting = false; // Set to true if the UI waits for the silentWork
		o.statusTracing = false; // true prevents adding new revisions to a file
		o.statUploads = 0; // complete count of logged upload-events
		o.userACReqs = 0; // AC-AutoComplete; prevents sending an API-request before the last returned
		o.userACReqsTimeOuts = 0; // prevents setting more than one timeout
		o.lecontinue = undefined;
		o.quota = {
			cycles: 0,
			time: 0,
			apiretry: 5,
			maxparsesize: 1000
		};

		if (mw.user && mw.user.tokens && mw.user.tokens.get) this.edittoken = mw.user.tokens.get('csrfToken');
		this.edittoken = (this.edittoken || window['wikilove-edittoken'] || (mw.user.isAnon() ? '+\\' : ''));

		// Preparing the UI
		$('#errorNode').remove();
		o.$errorNode = $('<div>', { id: 'errorNode', style: 'text-align:center;vertical-align:middle;min-height:130px;background:url(//upload.wikimedia.org/wikipedia/commons/8/84/Nuvola_apps_bad_kcontrol.png) #C44 no-repeat left; border-radius:10px; color:#FFF' }).prepend('An error occured. We regret the inconvenience. In order to improve this script, report the error-message below, please. ').append($('<a>', { href: 'javascript:JSONListUploads.autoErrorReport()' }).append('Try to automatically report the following message by clicking here, please.')).append('<br>').hide();
		$('#content').append(o.$errorNode);

		$('.tipsy').remove();

		o.gContainerCurrent = -1;
		o.gContainers = [];

		$('#gVersion').remove();
		var $gVersion = $('<div>', { id: 'gVersion', style: 'float:right;font: small-caps .8em Verdana;background:#ddd;' }).append(o.i18n.version + o.mdScriptRevision);
		$('#content').prepend($gVersion);

		$('#siteNotice').remove(); // takes too much place
		$('#bodyContent').remove();

		o.$gNotify = $('<div>', { id: 'gNotify' });
		o.$gNextHref = o.secureCall('createNextHref', 'nw');
		o.$gNextHref2 = o.secureCall('createNextHref', 's');

		o.$gLoader = $('<div>', { id: 'gStatusBar', style: 'text-align:center', text: 'Loading … (may take more than one minute)' })
			.prepend($.createSpinner({ id: 'sStatusBar', size: 'large' }), ' ');
		o.$bottomSpacer = $('<div>', { style: 'height:100px' }).text(' ').hide();
		o.$outerContainer = $('<div>', { id: 'gOuterContainer' }).append(o.secureCall('genGContainer'), o.$gLoader, o.$gNoResults, o.$bottomSpacer); // Must be called before genPagePanel

		var getMwDate = function (dateX) {
			if (typeof dateX !== 'string') return dateX;
			if (mwDateRx.test(dateX)) return dateX;
			var m1 = dateX.match(/(\d{4}).(\d{2}).(\d{2})\D*(\d{2}:\d{2}:\d{2})?/);
			if (m1) return (m1[1] + '-' + m1[2] + '-' + m1[3] + 'T' + (m1[4] ? m1[4] : '12:00:00') + 'Z');
			m1 = dateX.match(/(\d{2}).(\d{2}).(\d{4})\D*(\d{2}:\d{2}:\d{2})?/);
			if (m1) return (m1[3] + '-' + m1[2] + '-' + m1[1] + 'T' + (m1[4] ? m1[4] : '12:00:00') + 'Z'); else return o.i18n.invalidD;
		};

		$('#firstHeading').contents().first().replaceWith('Gallery tool: files uploaded by ' + (o.user || ' NOT SPECIFIED'));
		document.title = 'Uploads by ' + (o.user || ' NOT SPECIFIED') + ' - Gallery-Tool (JSONListUploads) - Wikimedia Commons';

		o.$gStatusBar = $('<div>', { id: 'gStatusBar', style: 'position:fixed; right:0; bottom:0; background:#eee' }).text('Preparing …').injectSpinner('sStatusBar');

		var $gFilerDesc = $('<span>').append(this.i18n.filtering, $('<sup>').append(o.$filterHelp = this.helpHover.clone().attr('title', this.i18n.filterHelp).tipsy({ gravity: 'nw' }))),
			// Settings Panel
			$settingsPanel = $('<div>', { id: 'settingsPanel' }),
			// General settings UI
			// User
			$gUser = o.$gUser = $('<input>', { value: o.user })
				.attr({ 'id': 'gUser', 'type': 'text', 'title': o.i18n.optUser, 'placeholder': 'User', 'class': 'controlpaneltextbox' })
				.on('keyup', function (e) {
					if (e.which === 13)
						o.secureCall('recallMe');
					else
						o.secureCall('doesUserExist');
				})
				.autocomplete({
					minLength: 2,
					source: function (request, callback) {
						o.seekUsers(request, callback);
					},
					select: function (e, ui) {
						o.seekUsers({ term: ui.item.value });
					}
				})
				.on('input', function () {
					o.secureCall('doesUserExist');
					o.seekUsers({ term: $(this).val() });
				})
				.on('blur', function (/* e*/) {
					o.secureCall('doesUserExist');
				}),
			$gUserL = $('<label>', {
				'for': 'gUser',
				'title': o.i18n.optUser,
				'html': ' &nbsp;User&nbsp;'
			}),
			$gUserB = o.$gUserB = $('<input>', {
				type: 'submit',
				value: o.i18n.submit
			})
				.click(function () {
					o.secureCall('recallMe');
				});

		// Start date
		$.datepicker.setDefaults($.datepicker.regional[cfg.wgUserLanguage]);
		var $gStartAt = o.$gStartAt = $('<input>').attr({
				'id': 'gStartAt',
				'type': 'text',
				'title': o.i18n.optStartAt,
				'placeholder': 'YYYY-MM-DD',
				'class': 'controlpaneltextbox',
				'value': o.lestart
			})
				.datepicker({
					changeYear: true,
					dateFormat: 'yy-mm-ddT12:00:00Z',
					showWeek: true,
					firstDay: 1
				})
				.on('blur', function (/* e*/) {
					$(this).val(getMwDate($(this).val()));
				})
				.on('keyup', function (e) {
					if (e.which === 13) {
						$(this).val(getMwDate($(this).val()));
						o.secureCall('recallMe');
					}
				})
				.tipsy({
					trigger: 'focus',
					gravity: 's',
					html: true,
					title: function () {
						return o.i18n.optStartAtHowTo;
					} }),
			$gStartAtL = $('<label>', {
				'for': 'gStartAt',
				'title': o.i18n.optStartAt,
				'html': o.i18n.start
			});

		// Sorting
		o.$gOptionOldestFirst = $('<select>', {
			'id': 'sorting',
			'class': 'controlpanelselect'
		})
			.append(
				$('<option>', { value: false }).append(o.i18n.newest),
				$('<option>', { value: true }).append(o.i18n.oldest)
			)
			.on('change', function () {
				o.oldestFirst = (this.value == 'true');
				o.secureCall('recallMe');
			});

		o.$gOptionOldestFirst2 = $('<select>', {
			'id': 'sorting2',
			'class': 'controlpanelselect'
		})
			.append(
				$('<option>', { value: false }).append(o.i18n.newest),
				$('<option>', { value: true }).append(o.i18n.oldest)
			)
			.on('change', function () {
				o.oldestFirst = (this.value == 'true');
				o.secureCall('recallMe');
			});

		var $gOptionOldestFirstL = $('<label>', { 'for': 'sorting', 'html': this.i18n.sorting }),
			$gOptionOldestFirst2L = $('<label>', { 'for': 'sorting2', 'html': this.i18n.sorting });
		o.$gOptionOldestFirst.val(String(o.oldestFirst)); o.$gOptionOldestFirst2.val(String(o.oldestFirst)); // '' converts to string

		var $generalSettings = $('<span>', { 'class': 'filter-frame' }).append($gUserL, $gUser, $gStartAtL, $gStartAt, $gOptionOldestFirstL, o.$gOptionOldestFirst, $gUserB),

			// View Mode - using bad-style for IE < 8
			$viewModeGallery = $('<input name="viewMode" type="radio" checked="checked">').attr({ 'value': '0', 'id': 'viewModeGallery', 'class': 'controlpanelcheckbox' }).change(
				function () { if (this.checked) { o.viewMode = this.value; o.secureCall('filterChanged'); } } // filterChanged removes all containers
			),
			$viewModeGalleryL = $('<label>', { 'for': 'viewModeGallery' }).append(this.i18n.vdetails),
			$viewModePure = $('<input name="viewMode" type="radio">').attr({ 'value': '1', 'id': 'viewModePure', 'class': 'controlpanelcheckbox' }).change(
				function () { if (this.checked) { o.viewMode = this.value; o.secureCall('filterChanged'); } } // filterChanged removes all containers
			),
			$viewModePureL = $('<label>', { 'for': 'viewModePure' }).append(this.i18n.hdetails),
			// var $viewModeList = $('<input>', { type: 'radio', 'name': 'viewMode', value: '2', checked: 'checked' });
			$viewMode = $('<span>', { 'class': 'filter-frame' }).append($viewModeGallery, $viewModeGalleryL, $viewModePure, $viewModePureL);

		$settingsPanel.append(
			$('<div>', { 'class': 'accordion ui-corner-all ui-state-hover', 'id': 'GeneralSettingsUI' }).append($('<a>', { href: '#' }).append(this.i18n.change)),
			$('<div>', { 'class': 'accordion-content ui-widget-content' }).append($generalSettings, ' ', $viewMode)
		);

		// Filter settings UI
		var $gInitial = o.$gInitial = $('<input type="checkbox" checked="checked">').attr({ 'id': 'gInitial', 'title': o.i18n.optIntit, 'class': 'controlpanelcheckbox' })
				.on('change', function () { o.secureCall('filterChanged'); }),
			$gInitialL = $('<label>', { 'for': 'gInitial', 'title': o.i18n.optIntit })
				.append($('<img>', { src: o.uActions.upload, height: 24, width: 24 })).append(o.i18n.created),
			$gInitialWrap = $('<span>', { 'class': 'filter-frame' }).append($gInitial, $gInitialL),

			$gOverwrite = o.$gOverwrite = $('<input type="checkbox" checked="checked">').attr({ 'id': 'gOverwrite', 'title': o.i18n.optOverwrite, 'class': 'controlpanelcheckbox' })
				.on('change', function () { o.secureCall('filterChanged'); }),
			$gOverwriteL = $('<label>', { 'for': 'gOverwrite', 'title': o.i18n.optOverwrite })
				.append($('<img>', { src: o.uActions.overwrite, height: 24, width: 27 })).append(o.i18n.overwrote),
			$gOverwriteWrap = $('<span>', { 'style': 'border-left:none;', 'class': 'filter-frame' }).append($gOverwrite, $gOverwriteL),

			$gRevert = o.$gRevert = $('<input type="checkbox" checked="checked">').attr({ 'id': 'gRevert', 'title': o.i18n.optRevert, 'class': 'controlpanelcheckbox' })
				.on('change', function () { o.secureCall('filterChanged'); }),
			$gRevertL = $('<label>', { 'for': 'gRevert', 'title': o.i18n.optRevert })
				.append($('<img>', { src: o.uActions.revert, height: 24, width: 24 })).append(o.i18n.reverted),
			$gRevertWrap = $('<span>', { 'style': 'border-left:none;', 'class': 'filter-frame' }).append($gRevert, $gRevertL),

			$gDelete = o.$gDelete = $('<input>', { 'id': 'gDelete', 'type': 'checkbox', 'title': o.i18n.optDeleted, 'class': 'controlpanelcheckbox' })
				.on('change', function () { o.secureCall('filterChanged'); }),
			$gDeleteL = $('<label>', { 'for': 'gDelete', 'title': o.i18n.optDeleted }).append(o.i18n.deleted),
			$gDeleteWrap = $('<span>', { 'class': 'filter-frame' }).append($gDelete, $gDeleteL),

			$gHideRvs = o.$gHideRvs = $('<input type="checkbox" checked="checked">').attr({ 'id': 'gHideRvs', 'title': o.i18n.optHideRevs, 'class': 'controlpanelcheckbox' })
				.on('change', function () { o.secureCall('filterChanged'); }),
			$gHideRvsL = $('<label>', { 'for': 'gHideRvs', 'title': o.i18n.optHideRevs }).append(o.i18n.latest),
			$gHideRvsWrap = $('<span>', { 'class': 'filter-frame' }).append($gHideRvs, $gHideRvsL),

			$gTop = o.$gTop = $('<select>', { 'id': 'gTop', 'class': 'controlpanelselect' }).append(
				$('<option>', { value: 0 }).append(o.i18n.state),
				$('<option>', { value: 1 }).append(o.i18n.top),
				$('<option>', { value: 2 }).append(o.i18n.old)
			).on('change', function () { o.secureCall('filterChanged'); }),
			$gTopWrap = $('<span>', { 'class': 'filter-frame', 'title': o.i18n.optTop }).append($gTop),

			$gCat = o.$gCat = $('<input>')
				.attr({ 'id': 'gCat', 'type': 'text', 'placeholder': o.i18n.inCatPlaceholder, 'class': 'controlpaneltextbox' })
				.on('keyup', function (e) { if (e.which === 13) o.secureCall('filterChanged'); })
				.tipsy({ trigger: 'focus', gravity: 's', title: function () { return o.i18n.optCatHowTo; } })
				.autocomplete({
					minLength: 2,
					source: function (request, callback) { o.seekCats(request, callback); }
				}),
			$gCatB = $('<input>', { type: 'submit', value: o.i18n.apply }).click(function () { o.secureCall('filterChanged'); }),
			$gCatWrap = $('<span>', { 'class': 'filter-frame', 'title': o.i18n.optCat }).append($gCat, $gCatB);

		$settingsPanel.append(
			$('<div>', { 'class': 'accordion ui-corner-all ui-state-hover' }).append($('<a>', { href: '#' }).append($gFilerDesc)),
			$('<div>', { 'class': 'accordion-content ui-widget-content' }).append($gInitialWrap, $gOverwriteWrap, $gRevertWrap, ' ', $gCatWrap, ' ', $gDeleteWrap, ' ', $gHideRvsWrap, ' ', $gTopWrap)
		);

		o.$gOptions = $('<div>')
			.append($settingsPanel)
			.append(o.secureCall('createPagesPanel'), o.$gNextHref);
		o.$gOptions2 = $('<div>').append($gOptionOldestFirst2L, o.$gOptionOldestFirst2).append(o.secureCall('createPagesPanel'), o.$gNextHref2);

		var $bodyContent = $('<div>', { id: 'bodyContent' });
		$('#content').append($bodyContent.append(o.$gNotify, o.$gOptions, o.$outerContainer, o.$gOptions2, o.$gStatusBar));

		// Notifyer (Welcome and useful tools pupulator)
		if (mw.user && mw.user.getName && o.user) o.secureCall('wikiNotifyer', o.$gNotify);

		$('.accordion').click(function () {
			$(this).toggleClass('ui-state-active', 'ui-widget-header');
			$(this).next().toggle('fast');
			return false;
		}).next().hide();

		// For external listeners
		$doc.triggerHandler('JSONListUploads', ['evtObjectReady', o.$gOptions]);

		// Communication Interface: UI <-> background worker
		o.$inputs = $('input, select', $bodyContent);

		o.$gOptions.on('silentWork', function () {
			if (o.UIWaiting)
				o.$gOptions.triggerHandler('lockUI');

		});
		o.$gOptions.on('silentWorkUpdate', function (e, s) {
		// if (s) console.log(s);
			if (o.UIWaiting)
				o.secureCall('setStatus', s);
			else
				o.secureCall('setStatus', '');

		});
		o.$gOptions.on('silentWorkDone', function () {
			if (o.UIWaiting) o.secureCall('createPage');
			o.UIWaiting = false;
			o.$gOptions.triggerHandler('silentWorkUpdate');
			o.$gOptions.triggerHandler('unLockUI');
			o.secureCall('doSilentWork'); // Next round
		});
		o.$gOptions.on('lockUI', function () {
			o.$gLoader.fadeIn();
			o.$inputs.prop('disabled', true);
			o.$gNextHref.hide();
			o.$gNextHref2.hide();
		});
		o.$gOptions.on('unLockUI', function () {
			o.$gLoader.fadeOut();
			o.$inputs.prop('disabled', false);
		});

		// Finally let's go
		if (o.user) {
			o.UIWaiting = true;
			o.secureCall('doSilentWork');
		} else {
			this.secureCall('wrongUserSpecified', 'You are neither logged in nor a username is specified.');
		}
	},

	bindTipsyAndIbox: function ($nodes) {
		var o = this,
			$content = $nodes || $('#bodyContent');

		$content.find('.tipsymetadata').tipsy({
			title: function () {
				$('.tipsy').remove();
				return o.secureCall('mdGetMetaTable', $(this).parent().attr('tipsy-title'), o);
			},
			html: true,
			gravity: $.fn.tipsy.autoNS
		});
		$content.find('.tipsyrevision').tipsy({
			title: function () {
				$('.tipsy').remove();
				return o.secureCall('mdGetRevisionTable', $(this).parent().attr('tipsy-title'), o);
			},
			html: true,
			gravity: $.fn.tipsy.autoNS
		});
		$content.find('.tipsycategory').tipsy({
			title: function () {
				$('.tipsy').remove();
				return o.secureCall('mdGetCatTable', $(this).parent().attr('tipsy-title'), o, false).html();
			},
			html: true,
			delayOut: 1000,
			gravity: $.fn.tipsy.autoNS
		});
		$content.find('.pureImgPopUp').ibox({
			callBack: function ($el/* , el*/) {
				var html = o.secureCall('getPureImgDetails', $el.attr('imgID')),
					nodes = $.parseHTML(html);
				o.bindTipsyAndIbox($(nodes));
				return $nodes;
			},
			close: function () {
				$('.tipsy').remove();
			},
			rmClass: 'pureImgPopUp',
			closeOnLeaveOf: o.$outerContainer,
			node: o.$outerContainer
		});
	},

	wrongUserSpecified: function (text) {
		text = text || 'The user you have specified does not exist. Fill in a valid username, please.';
		this.$gOptions.triggerHandler('unLockUI');
		$('#GeneralSettingsUI').click();
		this.secureCall('setStatus', text);
		this.$gLoader.text(text);
		this.$gLoader.stop().fadeTo('fast', 1);
	},

	/* Get a list of logevents of spec. user */
	findUploads: function () {
		if (this.listUploadsPending !== 0 || this.uploadsToProcess.length !== 0 || this.openUploads.length !== 0) {
			this.lastTask();
			return;
		}
		var query = {
			action: 'query',
			list: 'logevents',
			rawcontinue: 1,
			ledir: (this.oldestFirst ? 'newer' : 'older'),
			leprop: 'title|type|timestamp',
			letype: 'upload',
			leuser: this.user,
			lelimit: 40
		};
		if (this.lecontinue !== '') query.lecontinue = this.lecontinue;
		if (this.lestart !== '') query.lestart = this.lestart;

		this.$gOptions.triggerHandler('silentWorkUpdate', ['waiting for upload list …']);
		this.listUploadsPending++;
		this.doAPICall(query, 'findUploadsCB');
	},

	findUploadsCB: function (result/* , query*/) {
		this.listUploadsPending--;
		var rv,
			uItem,
			les = result.query.logevents,
			o = this;

		$.each(les, function (id, le) { // loop through all filechanges (array)
			le = les[id];
			uItem = o.getUploadsRevItem(le.title, le.timestamp);
			le.timestamp = uItem[1]; // update timestamp if there is a revision with nearly the same
			rv = uItem[0];

			rv.le = le; // should be unique; logid may another possib.
			rv.byUser = true;

			if (o.uploadMap[le.title]) { // already queried or in queue
				if (!rv.ii && o.uploadMap[le.title].del) {
					rv.del = true;
					rv.iId = o.uploadMap[le.title].iId + '?';
				} else if (!rv.ii && o.uploadMap[le.title].queried && !rv.movedTo) {
					rv.delRev = true;
					rv.iId = o.uploadMap[le.title].iId + '?';
				}
			} else {
				o.openUploads.push(le.title); // get imageinfo
			}
			var iuMap = o.getUploadMapItem(le.title, le.timestamp);
			iuMap.le = le;

			o.uploadsToProcess.push(le.title + le.timestamp);

			o.statUploads++;
		});
		if (!result['query-continue']) this.noMoreFiles = true;
		if (!this.noMoreFiles) this.lecontinue = result['query-continue'].logevents.lecontinue;
		this.nextTask();
	},

	/* Query ImageInfo. Get detailed information about an image. */
	queryII: function () {
		var o = this;
		if (o.listUploadsPending !== 0 || (o.noMoreFiles && !o.uploadsToProcess.length)) {
			o.lastTask();
			return;
		}
		if (!this.openUploads.length && !o.cachedQuery) {
			o.secureCall('queryIIComplete', {}, {});
			return;
		}

		var query = o.cachedQuery || {
			action: 'query',
			prop: 'imageinfo|info|categories',
			rawcontinue: 1,
			iiprop: 'url|size|metadata|timestamp|user|sha1|comment|mime',
			iilimit: 500,
			iiurlwidth: 120,
			iiurlheight: 120,
			titles: o.openUploads.join('|'),
			clprop: 'hidden',
			cllimit: 500,
			intoken: (o.edittoken ? '' : 'edit')
		};

		// Something new sophisticated because of https://phabricator.wikimedia.org/T48551
		// TODO: Use new continuation through gadget libAPI (encouraged by the mw-API-devs)
		if (o.querycontinue) {
			var props = [];
			$.each(o.querycontinue, function (k, v) {
				props.push(k);
				$.extend(query, v);
			});
			query.prop = props.join('|');
		} else {
			o.cachedQuery = query;
		}

		o.openUploads = [];
		o.$gOptions.triggerHandler('silentWorkUpdate', ['waiting for detailed information …']);
		o.doAPICall(query, 'queryIICB');
	},

	queryIICB: function (r/* , q*/) {
	// check whether query for imageinfo is complete
	// don't forget to reset "querycontinue" before starting the next cycle
		var o = this,
			querycontinue = o.querycontinue = r['query-continue'];

		// The nice thing about $.extend is that it also extends arrays!
		if (!o.cachedResult)
			o.cachedResult = r;
		else
			$.extend(true, o.cachedResult, r);

		if (querycontinue) {
			o.$gOptions.triggerHandler('silentWorkUpdate', ['got a query-continue …']);
			o.secureCall('queryII');
		} else {
			o.secureCall('queryIIComplete', o.cachedResult, o.cachedQuery);
		}
	},

	/* Helper function: Extract categories from the list of those. */
	mdExtractTags: function (mdCats) {
		var	thiscat,
			mdTags = '',
			_this = this,
			skip = false;

		if (typeof mdCats !== 'object') return mdTags;
		mdTags += '<i>';

		// First the licenses (hidden):
		$.each(mdCats, function (i, cati) {
			if (typeof cati.hidden === 'string') { // it is an empty string if true
				thiscat = cati.title;
				skip = false;
				$.each(_this.mdLicenseRecognization, function (id, recogi) {
					if (recogi[0].test(thiscat)) {
						if (recogi[1] !== '')
							mdTags += '<abbr title="' + thiscat.replace('Category:', '') + '">' + recogi[1] + '</abbr> ';

						skip = true;
						return false;
					}
				});
				if (skip) return;

				// This will be only executed if there was no match
				mdTags += '<abbr title="' + thiscat.replace('Category:', '') + '">U</abbr> ';
			}
		});

		mdTags += '</i> <span style="background-color:#FC9;">';

		// Then, deletion tags:
		$.each(mdCats, function (i, cati) {
			thiscat = cati.title;
			skip = false;
			$.each(_this.mdTagRecognization, function (id, thistag) {
				if (thistag[0].test(thiscat)) {
					if (thistag[1] !== '')
						mdTags += (thistag[2] ? '<span style="background-color:#' + thistag[2] + ';">' : '') + '<abbr title="' + thiscat.replace('Category:', '') + '">' + thistag[1] + '</abbr>' + (thistag[2] ? '</span>' : '');

					skip = true;
					return false;
				}
			});
			if (skip) return;
		});

		mdTags += '</span>';

		return mdTags;
	},

	/* Helper function: Creating a gallery box for gallery view */
	mdCreateGalleryBox: function (img, txt, height, style, imgTitle, imgH, addGU, title) {
		var $ih = $('<div>', { style: 'margin:' + (imgH ? (Math.round((height - imgH) / 2) + 'px auto;') : '15px auto;') }),
			$th = $('<div>', { 'class': 'gallerytext', 'tipsy-title': imgTitle }),
			$gbGU = $('<div>', { 'class': 'jGU', 'title': 'Global Usage - Usage of this file on other wikis' }),
			$galleryBox = $('<li>', { 'style': 'width:155px; background:#f8f8f8;', 'class': 'gallerybox' }).append($('<div>', { style: 'width: 155px' }).append($('<div>', { 'style': 'width: 150px;', 'class': 'thumb jImage' }).css('height', height).append($ih, $gbGU)).append($th));
		if (typeof style === 'string') $th.attr('style', style);

		$.each(img, function (id, imgi) {
			$ih.append(imgi);
		});
		$.each(txt, function (id, txti) {
			$th.append(txti);
		});
		// //////////
		if (addGU) {
			var gu = this.uploadMap[title].gu;
			if (gu) {
				$gbGU.badge(gu.length);
			} else {
				$gbGU.badge('?');
				$gbGU.needsContent = true;
			}
			$galleryBox.$gbGU = $gbGU;
		}
		// //////////
		return $galleryBox;
	},

	/* Helper function: ImageBox for pure-image view */
	mdCreateImageBox: function (img, width, rvID) {
		var $ih = $('<div>', { 'imgID': rvID, 'class': 'pureImgPopUp plainlinks' }),
			$galleryBox = $('<li>', { 'style': 'height: 120px', 'class': 'gallerybox' }).css('width', width).append($ih);

		$.each(img, function (id, imgi) {
			$ih.append(imgi);
		});
		return $galleryBox;
	},

	fillGlobalUsage: function () {
		mw.loader.using('ext.gadget.GlobalUsage', function () {
			JSONListUploads.secureCall('_fillGlobalUsage');
		});
	},
	_fillGlobalUsage: function () {
		var o = this,
			gu = window.mw.libs.GlobalUsage(5, 15, 3, false);

		gu.query(o.qGlobalUsage).progress(function (msg, pus, gus, pusPos) {
			$.each(o.qGlobalUsage, function (i, $el) {
				$el.off('click');
			});
			$.each(pusPos, function (name, gu) {
				var mapItem = o.uploadMap[name];
				// Maybe new files were added in-between
				if (!mapItem) return;

				mapItem.gu = gu;
				// Because this script has multiple file revs
				// it happens that only the last one gets a badge
				// by mw.libs.GlobalUsage
				// Also all elements are just cloned
				// Fix this also.
				var $gbGU;
				$.each(mapItem.gbs, function (i, $el) {
				// badge does not return the element!
				// https://bugzilla.wikimedia.org/show_bug.cgi?id=39383
					$el.$gbGU.data('globalUsage', gu).badge(gu.length);
					$el.$gbGU.data('globalUsageFN', name);
				});
				$.each(mapItem.gbClones || {}, function (i, $el) {
				// badge does not return the element!
				// https://bugzilla.wikimedia.org/show_bug.cgi?id=39383
					$gbGU = $el.find('div.jGU');
					$gbGU.data('globalUsage', gu).badge(gu.length);
					$gbGU.data('globalUsageFN', name);
				});
			});
			o.qGlobalUsage = {};
		}).done(function () {
			$('.jGU > div.notification-badge').tipsy({
				title: function () {
					if (!$(this).parents().data('globalUsage')) return '';
					return gu._getTipsyContent.apply(this, [5]);
				},
				delayOut: 1500,
				html: true,
				gravity: 'se'
			});
		});
	},

	/** Called by Tipsy on hovering. Get a list of cats for a specific file.
	@param {string} revision-identifier
	@param {Object} reference to JSONListUploads
	@param {boolean} render as flickr-like tags?
**/
	mdGetCatTable: function (rv, o, asTags) {
		var cats = o.uploadsRevs[rv].cats,
			$catWrap = $('<div>'),
			$catsList = $('<ul>');

		if (asTags)
			$catsList.addClass('j-cat-wrap');
		else
			$catWrap.html('<b><i>Categories:</i></b><br><br>');

		$catWrap.append($catsList);

		$.each(cats, function (id, tCat) {
			if (typeof cats[id].hidden === 'undefined') {
				$catsList.append(
					$('<li>', { 'class': (asTags ? ' j-cat-label' : '') }).append($('<a>',
						{	'class': (asTags ? ' j-cat-label' : ''),
							'href': mw.util.getUrl(tCat.title),
							'target': '_blank'
						}).text(tCat.title.replace('Category:', '')), ' '));
			}
		});
		return $catWrap;
	},

	titleFromRevKey: function (rk) {
		return rk.slice(0, rk.length - 20);
	},

	/* Called by Tipsy on hovering. Get a list of revisions for a specific file */
	mdGetRevisionTable: function (rv, o) {
		var t = this.titleFromRevKey(rv),
			time = rv.slice(rv.length - 20),
			res = '<b><i>Revisions:</i></b><br>',
			_this = this;

		$.each(o.uploadMapPureRev[t], function (id, it) {
			if (it) {
				if (it.size) { // image revision
					if (it.timestamp === time)
						res += '<i><small><span style="color:#0C9">' + _this.getDateFromMWDate(it.timestamp).toLocaleString() + '</span></small></i><br>';
					else
						res += '<i><small>' + _this.getDateFromMWDate(it.timestamp).toLocaleString() + '</small></i><br>';

					res += '<small>' + mw.html.escape(it.comment.slice(0, 100));
					res += '<br><b>' + mw.html.escape(it.user) + '</b> &nbsp; &nbsp;' + _this.mdFormattNumber(it.size >> 10) + '&nbsp;KiB<br><br></small>';
				}
			}
		});
		return res;
	},

	/* Called by Tipsy on hovering. Get a list of metadata for a specific file */
	mdGetMetaTable: function (rv, o) {
		var	val,
			mdata = o.uploadsRevs[rv].ii.metadata,
			fRestr = (mdata.length > 6),
			stMdat = '<b><i>File-Metadata:</i></b><br><br>',
			restrArr = ['ImageDescription', 'Make', 'Model', 'Copyright', 'DateTime', 'Artist', 'Title', 'Author', 'Creator', 'CreationDate'];

		$.each(mdata, function (id, mdatai) {
			if (typeof mdatai.value !== 'string') return;
			try {
				val = mw.html.escape(mdatai.value).slice(0, 200);
			} catch (ex) {
				val = mdatai.value;
			}
			stMdat += ((fRestr && ($.inArray(mdatai.name, restrArr) === -1)) ? '' : '<i>' + mdatai.name + '</i><br>' + val + '<br><br>');
		});
		return stMdat;
	},

	/* Helper function: Called by QueryIICB. Get model info from Image-Metadata */
	mdGetCamModel: function (uItem) {
		if (!uItem || !uItem.ii.metadata) return '';

		var model = '',
			mdata = uItem.ii.metadata;
		if (typeof mdata !== 'object') return '';
		$.each(mdata, function (id, mdatai) {
			if (mdatai.name === 'Model') {
				model = $.trim(mdatai.value) + ' ';
				return false;
			}
		});
		return model;
	},

	/** Helper. Called when hovering an image in pure-image-view;
	@param {string} Revision Identifier
**/
	getPureImgDetails: function (rv) {
		var uItem = this.uploadsRevs[rv],
			camModel = (window.JSONListUploadsShowModel) ? this.secureCall('mdGetCamModel', uItem) : '',
			topState = (uItem.isOnTop ? (uItem.isOnTop === 1 ? '(Top)' : '(Top*)') : '(Old)'),
			newTitle = (uItem.movedTo || uItem.le.title),
			$stateIndicator = $('<span>', { style: 'font-weight: bold;' });
		if (uItem.del) {
			$stateIndicator.text('deleted');
		} else {
			$stateIndicator.append($('<a>', { href: mw.util.getUrl(newTitle), target: '_blank' })
				.text('deleted revision'));
		}

		return ($('<span>').append($('<img>', { src: this.uActions[uItem.le.action], style: 'float:right;' })).html() +
		'<span class="jPureTitle">' + mw.html.escape(newTitle.replace(/^File:/, '').replace(/\.[\w]{3,4}$/, '')) + '</span>' +
		'<div class="jFileTime">' + this.getDateFromMWDate(uItem.le.timestamp).toLocaleString() + '</div>' +
		(uItem.ii ? ('<div class="jFileSize">' +
			this.secureCall('mdFormattNumber', uItem.ii.size >> 10) + '&nbsp;<abbr title="1 KibiByte= 1024 Bytes">KiB</abbr>&nbsp; ' +
			this.secureCall('mdFormattNumber', uItem.ii.width) + '&nbsp;×&nbsp;' +
			this.secureCall('mdFormattNumber', uItem.ii.height) + 'px</div>' +
			$('<div>').append(this.secureCall('mdGetCatTable', rv, this, true), $('<span>', { 'tipsy-title': rv }).append(
				$('<a>', { href: 'javascript:JSONListUploads.setUpDialog(\'' + encodeURIComponent(newTitle).replace(/'/g, '&#39;') + '\')', title: 'Detailed information.' }).text('details'), ' ',
				($.isArray(uItem.ii.metadata) ?
					$('<span>', { 'class': 'tipsymetadata' }).text('meta') :
					''), (camModel ? ('&nbsp;' + camModel) : ''), ' ',
				$('<span>', { 'class': 'tipsyrevision' }).text(topState), '<br>',
				(this.secureCall('mdExtractTags', uItem.cats) + '<br>')
			)).html()) :
			($('<div>').append($stateIndicator).html())
		)
		);
	},

	/* Format a number > 1 (1000 --> 1 000) */
	mdFormattNumber: function (iNr) {
		iNr = String(iNr);
		var rx = /(\d+)(\d{3})/;
		while (rx.test(iNr))
			iNr = iNr.replace(rx, '$1<span style="font-size:40%;font-weight:100">&nbsp;</span>$2');

		return iNr;
	},

	getOrdStringFromNumber: function (nr, big) {
	// 0 -> a (97) … z (122) // base10 --> "base26" // 701 --> zz; 702 --> aaa; 702 --> aab
		big = big ? 65 : 97;
		var arrNrs = [],
			rem = 0;
		while (nr > 25) {
			rem = nr % 26;
			arrNrs.unshift((rem + big));
			nr = Math.floor(nr / 26) - 1;
		}
		arrNrs.unshift((nr + big));
		return String.fromCharCode.apply(this, arrNrs);
	},

	/** 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$/,
			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?
	},

	getTimestamp: function (stTitle, stTimestamp) {
		var tolerance = 42 * 1000, // 42 seconds
			thisDate = this.getDateFromMWDate(stTimestamp),
			foundDate = false;

		for (var ts in this.uploadMap[stTitle]) {
			var rvi = this.uploadMap[stTitle][ts];
			if (typeof rvi !== 'object' || $.isArray(rvi)) continue;
			var thatDate = this.getDateFromMWDate(ts);
			if (Math.abs(thisDate - thatDate) < tolerance) {
				foundDate = ts;
				break;
			}
		}
		return [(foundDate || stTimestamp), (typeof foundDate === 'string')];
	},

	/* Helper. Hands out or creats a revision-object */
	getUploadsRevItem: function (stTitle, stTimestamp) {
		var ts = this.secureCall('getTimestamp', stTitle, stTimestamp);
		if (ts[1])
			return [this.uploadsRevs[stTitle + ts[0]], ts[0]];
		else
			return [(this.uploadsRevs[stTitle + ts[0]] = { title: stTitle }), ts[0]]; // create a new object

	},

	/* Helper. Hands out or creats a map-object */
	getUploadMapItem: function (stTitle, stTimestamp) {
		if (!this.uploadMap[stTitle]) {
			this.uploadMap[stTitle] = {};
			this.uploadMap[stTitle].iId = this.iUploads;
			this.iUploads++;
		}
		if (!this.uploadMap[stTitle][stTimestamp])
			this.uploadMap[stTitle][stTimestamp] = {};

		return this.uploadMap[stTitle][stTimestamp];
	},

	/** Helper. Adds a pure-rev-item (not called by listUploadsCB) to the end of the list to get an ordered list.
	@param {string} stTitle  The filename.
	@param {string} stTimestamp  The revision-timestamp (upload-date).
	@param {Object} uRvItem  Item containing image-(revision)-info.
**/
	pushUploadMapPureRevItem: function (stTitle, stTimestamp, uRvItem) {
		if (!this.uploadMapPureRev[stTitle])
			this.uploadMapPureRev[stTitle] = {};

		if (!this.uploadMapPureRev[stTitle][stTimestamp])
			this.uploadMapPureRev[stTitle][stTimestamp] = {};

		this.uploadMapPureRev[stTitle][stTimestamp] = uRvItem;
	},

	/** Helper. Add a scaled image to a revision-object.
	@param {Object} rvItem  The revision-Item.
	@param {number} hDesired  The desired maximal height.
	@param {number} wDesired  The desired maximal width.
	@param {string} attachAs  Name of the new member to be attached to rvItem.
**/
	scale: function (rvItem, hDesired, wDesired, attachAs) {
	// for pure-image-mode
		var browserCompatTypes = {
				'image/gif': '', 'image/jpeg': '', 'image/png': ''
			},
			urlScaleToW = function (w) {
				return rvItem.thumburl.replace(/\d+px-/, (w + 'px-'));
			},
			si = rvItem[attachAs] = { url: rvItem.url };
		if (rvItem.height > hDesired || rvItem.width > wDesired) {
		// Scale
			var aspD = hDesired / wDesired,
				aspA = rvItem.height / rvItem.width;
			if (aspD < aspA) {
			// Desired ratio is higher than the actual ratio.
				si.height = hDesired;
				si.width = Math.round(hDesired / aspA);
				si.url = urlScaleToW(si.width);
			} else {
				si.height = Math.round(wDesired * aspA);
				si.width = wDesired;
				si.url = urlScaleToW(si.width);
			}
		} else {
		// Original if compat
			si.height = rvItem.height;
			si.width = rvItem.width;
			if (!(rvItem.mime in browserCompatTypes))
				si.url = urlScaleToW(si.width);

		}
	},

	/** Processing image-info data, creating gallery-boxes and save them in a variable.
		@param {Object} result  The result of
		@param {Object} query  The query for detailed ImageInfo
	**/
	queryIIComplete: function (result/* , query*/) {
		var	t,
			id,
			o = this;

		o.cachedQuery = undefined;
		o.cachedResult = undefined;
		o.querycontinue = undefined;

		if (!result.query) {
			o.$gOptions.triggerHandler('silentWorkUpdate', ['no detailed information - skipping…']);
			o.nextTask();
			return;
		} else {
			o.$gOptions.triggerHandler('silentWorkUpdate', ['got detailed information - processing…']);
		}

		var pages = result.query.pages,
			getOldThumbUrl = function (rv) {
				var url = rv.url;
				if (!url) { // hidden revision
					return ['//upload.wikimedia.org/wikipedia/commons/e/e7/Toolbaricon_hidden.png', 23, 22];
				}
				var typereplace = {
						ogg: ['//upload.wikimedia.org/wikipedia/commons/e/ef/Farm-Fresh_file_extension_ogg.png', 32, 32],
						wav: ['//upload.wikimedia.org/wikipedia/commons/6/67/Crystal_sound.png', 64, 64],
						flac: [cThumb + '2/29/FLAC_logo.png/120px-FLAC_logo.png', 120, 60],
						oga: ['/static/current/resources/assets/file-type-icons/fileicon-ogg.png', 120, 120]
					},
					typemap = {
						jpg: 'jpg', jpeg: 'jpg', pdf: 'jpg', djvu: 'jpg', ogv: 'jpg',
						png: 'png', svg: 'png', tif: 'png', tiff: 'png',
						gif: 'gif'
					},
					ext = url.slice(url.lastIndexOf('.') + 1).toLowerCase();

				if (ext in typereplace) return typereplace[ext];

				var thmbUrl = url.replace('wikipedia/commons/', 'wikipedia/commons/thumb/');
				thmbUrl += '/';
				switch (ext) {
					case 'djvu':
					case 'pdf': thmbUrl += 'page1-'; break;
					case 'ogv': thmbUrl += 'mid-'; break;
					case 'tiff':
					case 'tif': thmbUrl += 'lossless-page1-'; break;
				}
				var widthPx = rv.width,
					heightPx = rv.height;
				if (rv.width > 120 || rv.height > 120) { // we have to scale down
					if (rv.width >= rv.height) {
						widthPx = 120;
						heightPx = Math.round(120 * rv.height / rv.width);
					} else {
						widthPx = Math.round(rv.width * 120 / rv.height);
						heightPx = 120;
					}
				}
				thmbUrl += widthPx + 'px-' + encodeURI(dirtyTitle.replace(/^File:/, ''));

				try { thmbUrl += '.' + typemap[ext]; } catch (ex) {}
				return [thmbUrl, widthPx, heightPx];
			};

		// We queried some pages; loop through them
		for (id in pages) {
			var pg = pages[id], // a single page
				dirtyTitle = o.statusTracing ? o.uploadMoves[pg.title] : pg.title,
				rvi;

			o.uploadMap[dirtyTitle].queried = true;

			if (!pg.imageinfo) { // deleted (rev del- check follows after processing)
				for (t in o.uploadMap[dirtyTitle]) {
					rvi = o.uploadMap[dirtyTitle][t];
					if (typeof rvi !== 'object' || $.isArray(rvi)) continue;
					o.uploadsRevs[dirtyTitle + t].del = true;
					o.uploadsRevs[dirtyTitle + t].iId = o.uploadMap[dirtyTitle].iId + '?';
				}
				if (!o.statusTracing) o.uploadsToTrace.push(dirtyTitle);
				o.uploadMap[dirtyTitle].del = true;
				continue;
			}
			var cats = pg.categories || [],
				seenHash = {}, // Stolen from AjaxQuickDelete - testing whether there was a revision with the same hash
				usersLastRev = '',
				lastNotBotRev = '',
				shaLastRev = pg.imageinfo[0].sha1;
				// currID = 0;

			for (var iRv = pg.imageinfo.length - 1; iRv !== -1; iRv--) { // Multiple revisions in reverse order
				var rv = pg.imageinfo[iRv];

				// Sanitizing output in case of tricky deletions
				if (!rv.comment) rv.comment = '';
				if (!rv.user) rv.user = '<hidden/>';

				var uArrRev = o.getUploadsRevItem(dirtyTitle, rv.timestamp);
				rv.timestamp = uArrRev[1]; // update timestamp if there was a log entry with nearly the same stamp
				var uRev = uArrRev[0];

				if (rv.user === o.user) {
					uRev.byUser = true;
					usersLastRev = rv.timestamp;
				} else {
					uRev.byUser = false;
				}

				if (uRev.byUser) {
					var irRv = pg.imageinfo.length - 1 - iRv;
					uRev.iId = o.uploadMap[dirtyTitle].iId + ((pg.imageinfo.length > 1) ? ('<abbr title="This is revision ' + (irRv + 1) + ' of this file.">' + o.getOrdStringFromNumber(irRv)) + '</abbr>' : '');
				}

				if (typeof seenHash[rv.sha1] !== 'undefined') { // checking possible revert
					uRev.revert = true;
				} else {
					uRev.revert = false;
				}
				seenHash[rv.sha1] = true;

				if (rv.sha1 === shaLastRev) uRev.isOnTop = 3; // later reverted back to this rev.

				if (!(rv.user in o.botIgnorance)) lastNotBotRev = rv.timestamp;

				uRev.ii = rv;
				uRev.cats = cats;
				var iuMap = o.getUploadMapItem(dirtyTitle, rv.timestamp);
				iuMap.ii = rv;
				o.pushUploadMapPureRevItem(dirtyTitle, rv.timestamp, rv);

				if (!uRev.byUser) continue;

				// TODO: calculating Thumb-URL for old images; can be dropped out; was for MW 1.17

				if (!rv.thumburl) {
					var thmbMetrics = getOldThumbUrl(rv);
					rv.thumburl = thmbMetrics[0];
					rv.thumbwidth = thmbMetrics[1];
					rv.thumbheight = thmbMetrics[2];
				}

				// for pure-image-mode
				o.scale(rv, 120, 840, 'gbi');
			}
			o.uploadsRevs[dirtyTitle + lastNotBotRev].isOnTop = 2; // overwritten by a bot
			o.uploadsRevs[dirtyTitle + pg.imageinfo[0].timestamp].isOnTop = 1; // Not overwritten

			if (usersLastRev !== '') o.uploadsRevs[dirtyTitle + usersLastRev].isLastUserRev = true; // Last revision uploaded by specif. user
			for (t in o.uploadMap[dirtyTitle]) {
				var ul = o.uploadMap[dirtyTitle][t];
				if (typeof ul !== 'object' || $.isArray(ul)) continue;
				if (!ul.ii) {
					o.uploadsRevs[dirtyTitle + t].delRev = true;
					o.uploadsRevs[dirtyTitle + t].iId = o.uploadMap[dirtyTitle].iId + '?';
					if (!o.statusTracing && $.inArray(dirtyTitle, o.uploadsToTrace) === -1) o.uploadsToTrace.push(dirtyTitle);
				}
			}
		}
		this.nextTask();
	},

	traceUploads: function () {
		var query,
			_this = this;

		this.statusTracing = true;
		this.$gOptions.triggerHandler('silentWorkUpdate', ['tracing…']);

		$.each(this.uploadsToTrace, function (id, title) {
			query = {
				action: 'query',
				list: 'logevents',
				leprop: 'type|user|timestamp|details',
				ledir: 'older',
				lelimit: 100,
				letitle: title,
				requestid: title
			};
			_this.traceUploadsPending++;
			_this.doAPICall(query, 'traceUploadCB');
		});
		if (!this.traceUploadsPending) {
			this.$gOptions.triggerHandler('silentWorkUpdate', ['nothing to trace.']);
			this.nextTask();
		}
	},

	traceUploadCB: function (result, query) {
		this.traceUploadsPending--;
		var t = query.requestid, // Old Filetitle
			ts,
			rv,
			rvArr, // Revistion+Timestamp
			mI, // MapItem
			movedTo = '',
			// le, // a single logevent
			_this = this; // Parent object

		$.each(result.query.logevents, function (id, le) {
			switch (le.type) {
				case 'upload':
					if (movedTo && le.user === _this.user) {
						le.title = t;
						// Requesting revision item for old file-title
						rvArr = _this.secureCall('getUploadsRevItem', t, le.timestamp);
						rv = rvArr[0];
						le.timestamp = ts = rvArr[1];

						// Add movedTo-string for later record and processing
						rv.movedTo = movedTo;
						rv.del = false;
						rv.delRev = false;
						mI = _this.secureCall('getUploadMapItem', t, ts);
						mI.movedTo = movedTo;
						mI.del = false;
						mI.delRev = false;
						_this.uploadMoves[movedTo] = t;
						_this.openUploads.push(movedTo);
					}
					break;
				case 'delete':
					movedTo = '';
					break;
				case 'move':
					movedTo = le.move ? le.move.new_title : ''; // File:Api-Error moved without target.png
					break;
			}
		});

		if (!this.traceUploadsPending) {
			this.uploadsToTrace = [];
			this.$gOptions.triggerHandler('silentWorkUpdate', ['traced all. next task …']);
			this.nextTask();
		}
	},

	setupGalleryBoxes: function () {
		var o = this;
		// id = 0;

		this.$gOptions.triggerHandler('silentWorkUpdate', ['generating galleryboxes…']);
		$.each(o.uploadsToProcess, function (id, stItem) {

			var uItem = o.uploadsRevs[stItem],
				gb,

				dirtyTitle = uItem.title,
				newTitle = (uItem.movedTo || uItem.le.title);

			uItem.matchesFilter = o.secureCall('matchesFilter', uItem);
			if (uItem.matchesFilter) {
				o.openChachedFiles++;
				o.totalMatchingFiles++;
			}

			uItem.gbInUse = false;

			if (uItem.ii && uItem.byUser) { // Not deleted
				var camModel = (window.JSONListUploadsShowModel) ? o.secureCall('mdGetCamModel', uItem) : '',
					topState = (uItem.isOnTop ? (uItem.isOnTop === 1 ? '(Top)' : '(Top*)') : '(Old)'),
					$movedTag = (uItem.movedTo ? $('<abbr>', { style: 'float:left;', title: 'renamed/moved from ' + dirtyTitle }).text('r') : '');

				uItem.gb = gb = o.mdCreateGalleryBox(
					[$('<a>', { 'href': uItem.ii.descriptionurl, 'target': '_blank', 'class': 'image' })
						.append($('<img>', { src: uItem.ii.thumburl, width: uItem.ii.thumbwidth, height: uItem.ii.thumbheight, alt: dirtyTitle }))],
					[$movedTag,
						$('<i>', { style: 'width:50px;float:left;' }).html(uItem.iId + ': '),
						$('<a>', { href: 'javascript:JSONListUploads.setUpDialog(\'' + encodeURIComponent(newTitle).replace(/\'/g, '&#39;') + '\')', title: 'Detailed information.', style: 'color:#BB6;' }).html('&nbsp;<b>d</b>'), '&nbsp; ',
						$('<span>', { 'class': 'tipsycategory' }).text('c'), ' ',
						(($.isArray(uItem.ii.metadata) && !!uItem.ii.metadata.length) ?
							$('<span>', { 'class': 'tipsymetadata' })
								.text('m') :
							''), (camModel ? ('&nbsp;' + camModel) : '') + ' ',
						$('<span>', { 'class': 'tipsyrevision' }).text(topState), '<br>',
						(o.secureCall('mdExtractTags', uItem.cats) + '<br>'),
						$('<a>', { 'href': uItem.ii.descriptionurl, 'target': '_blank', 'class': 'image jFileTitle' })
							.text(newTitle.replace('File:', '').replace(/\.[\w]{3,4}$/, '')),
						$('<div>', { 'class': 'jFileMime' }).text(uItem.ii.mime),
						$('<div>', { 'class': 'jFileTime' }).text(uItem.le.timestamp.replace(/[T|Z]/g, ' ')),
						$('<div>', { 'class': 'jFileSize' }).html(
							o.secureCall('mdFormattNumber', uItem.ii.size >> 10) +
					'&nbsp;<abbr title="1 KibiByte= 1024 Bytes">KiB</abbr>&nbsp; ' +
					(o.secureCall('mdFormattNumber', uItem.ii.width) + '&nbsp;×&nbsp;' + o.secureCall('mdFormattNumber', uItem.ii.height)) +
					'px')
					],
					150,
					'background:url(' + o.uActions[uItem.le.action] + ') no-repeat 26px 0',
					stItem,
					uItem.ii.thumbheight,
					true,
					dirtyTitle
				);
				if (uItem.isOnTop && uItem.isOnTop !== 2) gb.children('div').children('.thumb').css('border-color', '#6D8').css('background-color', '#EFE');
				if (gb.$gbGU && gb.$gbGU.needsContent) {
					var mapItem = o.uploadMap[dirtyTitle];

					o.qGlobalUsage[newTitle] = gb.$gbGU.click(o.fillGlobalUsage);
					if (!mapItem.gbs) mapItem.gbs = [];
					mapItem.gbs.push(gb);
				}

				uItem.gbi = o.mdCreateImageBox(
					[$('<a>', { 'href': uItem.ii.descriptionurl, 'target': '_blank', 'class': 'image' })
						.append($('<img>', { src: uItem.ii.gbi.url, width: uItem.ii.gbi.width, height: uItem.ii.gbi.height, alt: dirtyTitle }))],
					uItem.ii.gbi.width,
					stItem
				);
			} else { // Deleted Item
				var $stateIndicator = $('<span>', { style: 'font-weight: bold;' });
				if (uItem.del) {
					$stateIndicator.text('deleted');
				} else {
					$stateIndicator.append($('<a>', { href: mw.util.getUrl(newTitle), target: '_blank' })
						.text('deleted revision'));
				}
				uItem.gb = o.mdCreateGalleryBox(
					[$('<a>', { href: scriptUrl + '?title=Special:Log&page=' + newTitle, target: '_blank' }).append($('<img>', { src: '//upload.wikimedia.org/wikipedia/commons/d/db/Icon_delete.png', width: 32, height: 32, alt: 'deleted', title: 'This file has been deleted.' }))],
					[$('<i>', { style: 'width:50px;float:left;' }).html(uItem.iId + ': '),
						$stateIndicator, $('<br>'),
						$('<div>', { 'class': 'jFileTitle' }).text(dirtyTitle.replace('File:', '')),
						(uItem.le.timestamp.replace(/[T|Z]/g, ' ') + '<br>')],
					150,
					'background:url(' + o.uActions[uItem.le.action] + ') no-repeat 26px 0',
					stItem,
					32
				);
				if (uItem.del) uItem.gb.children('div').children('.thumb').css('background-color', '#FEE');

				uItem.gbi = o.mdCreateImageBox(
					[$('<a>', { 'href': scriptUrl + '?title=Special:Log&page=' + newTitle, 'target': '_blank', 'class': 'image' })
						.append($('<img>', { src: '//upload.wikimedia.org/wikipedia/commons/d/db/Icon_delete.png', width: 32, height: 32, alt: 'deleted', title: 'This file has been deleted.', align: 'middle' }))],
					32,
					stItem
				);
			}
		});
		o.uploadsToProcess = [];
		if (o.noMoreFiles)
			if (!o.shownStats) { o.secureCall('setupStats'); }

		// setupGalleryBoxes creates new 'tipsymetadata', 'tipsyrevision',
		// 'tipsycategory', and 'pureImgPopUp' nodes.
		// These will be attached (and tipsy bound) by createPage().
		this.nextTask();
	},

	/* Called when the list of images is complete. */
	setupStats: function () {
		var cr = 0,
			ow = 0,
			rvt = 0,
			del = 0,
			delrev = 0;
			// filterc = 0;
		for (var id in this.uploadsRevs) {
			if (this.uploadsRevs[id].le && this.uploadsRevs[id].byUser) {
				var leI = this.uploadsRevs[id].le,
					uI = this.uploadsRevs[id];
				switch (leI.action) {
					case 'upload': cr++; break;
					case 'overwrite': ow++; break;
					case 'revert': rvt++; break;
				}
				if (uI.del) del++;
				if (uI.delRev) delrev++;
			}
		}
		if (this.$gStatContainer) this.$gStatContainer.remove();
		if (this.$statNotifier) this.$statNotifier.remove();

		var $gHelpHover = this.helpHover.clone().attr('title', this.i18n.statResultHelp).tipsy({ gravity: 'ne' }),
			$gDummy = $('<span>', { style: 'visibility:hidden; color:#FFF; background-color: #FFF; padding: 0.0em 0.4em; font-size: 80%; border:none 1px #FFF' }).append('?'),
			$gStatContainer = this.$gStatContainer = $('<div>', { 'class': 'floatright', 'style': 'text-align:right; border:#f0c78A solid 1px;background-color: #E8F2F8; background-image: -moz-linear-gradient(right, #E8F2F8, #FFF); background-image: -ms-linear-gradient(right, #E8F2F8, #FFF); background-image: -o-linear-gradient(right, #E8F2F8, #FFF); background-image: -webkit-gradient(linear, right bottom, left bottom, from(#E8F2F8), to(#FFF));width:14em;' }).append(
				$('<div>', { 'class': 'floatright' }).append($gHelpHover, ' <br>' + cr + '<br>' + ow + '<br>' + rvt +
				'<br><b>' + this.statUploads + '</b><br>' + del + '<br>' + delrev + '<br>' + this.totalMatchingFiles),
				$('<div>', { 'class': 'floatleft' }).append('Statistics', $gDummy, ' <br>New Created: <br>Overwritten: <br>Reverted: ' +
				'<br><b>Total logged events: </b><br>Deleted: <br>Deleted revs: <br>Matching filter: ')
			).hide(),
			$statNotifier = this.$statNotifier = $('<div>', { 'class': 'floatright', 'style': 'text-align:right;' }).append($('<button>').append('Statistics').click(
				function () {
					$(this).parent().hide();
					$gStatContainer.fadeIn();
				}
			)),
			mouseTimeout = 0;
		$gStatContainer.mouseenter(function () {
			clearTimeout(mouseTimeout);
			$gStatContainer.stop().fadeTo('fast', 1);
		});
		$gStatContainer.mouseleave(function () {
			mouseTimeout = setTimeout(function () {
				$gStatContainer.fadeOut('slow', function () {
					$statNotifier.show();
				});
			}, 4000);
		});
		$('#bodyContent').prepend($statNotifier, $gStatContainer);
		$statNotifier.fadeIn('slow');
		this.$filterHelp.attr('title', this.i18n.filterHelp2);
	},

	/* Adds gallery-boxes to the current container. */
	createPage: function () {
		var i = 1,
			o = this;
		o.$gOptions.triggerHandler('silentWorkUpdate', ['setting up gallery …']);
		this.$gOptions.triggerHandler('unLockUI');
		for (var id in o.uploadsRevs) {
			var gItem = o.uploadsRevs[id],
				mapItem = o.uploadMap[gItem.title];

			if (!mapItem.gbClones) mapItem.gbClones = [];
			if (!gItem.gbInUse && gItem.matchesFilter) {
				if (i > 24) break;
				gItem.gbInUse = true;
				switch (o.viewMode) {
					case '1': if (gItem.gbi)
						o.$gContainer.append(gItem.gbi.clone());
						break;
					case '2': break;
					default: if (gItem.gb) {
					// event handler for query-global-usage
						var $clone = gItem.gb.clone(true);
						mapItem.gbClones.push($clone);
						o.$gContainer.append($clone);
					} break;
				}
				o.openChachedFiles--;
				i++;
			}
		}
		if (i === 1)
			$('.gNoResult', o.$gContainer).fadeIn();

		o.secureCall('replacePanels');
		if (!((i === 1 || !o.openChachedFiles) && o.noMoreFiles)) {
			o.$gNextHref.show().css('display', 'inline');
			o.$gNextHref2.show().css('display', 'inline');
		} else {
			o.openChachedFiles = 0;
		}
		o.bindTipsyAndIbox();
		setTimeout(function () { // doSilentWork should not be invoked before this stack is ready
			o.secureCall('doSilentWork');
		}, 1);
	},

	/** The detailed-information-dialog.
	*	@param {string} Filename
	*/
	setUpDialog: function (file) {
		file = decodeURIComponent(file.replace(/\&\#39\;/g, '\''));
		var req = 0,
			currentHTML = '';
		this.$dlgContainer = {};
		this.$dlgContainer = $('<div>', { id: 'JSONFileContainer', style: 'border: #999 solid 1px; background: #fff' }).append($('<div>', { id: 'JSONFileContainerInner' }).append($('<ul>').append(
			$('<li>').append($('<a>', { href: scriptUrl + '?title=Special:Log&page=' + encodeURIComponent(file) })
				.append($('<span>').append('Log'))),
			$('<li>').append($('<a>', { href: scriptUrl + '?action=render&title=' + encodeURIComponent(file) })
				.append($('<span>').append('Page-content'))),
			$('<li>').append($('<a>', { href: scriptUrl + '?action=raw&title=' + encodeURIComponent(file) })
				.append($('<span>').append('Wikitext'))),
			$('<li>').append($('<a>', { href: scriptUrl + '?title=Special:GlobalUsage/' + encodeURIComponent(file.replace('File:', '').replace(/ /g, '_')) })
				.append($('<span>').append('Global usage')))
		)).tabs({
			event: 'mouseover',
			spinner: 'Loading content …',
			beforeLoad: function (event, ui) {
				var settings = ui.ajaxSettings,
					jqXHR = ui.jqXHR;

				if (ui.tab.data('loaded')) {
					event.preventDefault();
					return;
				}
				jqXHR.done(function () {
					ui.tab.data('loaded', true);
				});
				if (settings.url.indexOf('Special:GlobalUsage') !== -1) {
					settings.dataType = 'text';
					req = 2;
				} else if (settings.url.indexOf('Special:Log') !== -1) {
					settings.dataType = 'text';
					req = 3;
				} else if (settings.url.indexOf('action=raw') !== -1) {
					settings.dataType = 'text';
					req = 4;
				} else {
					req = 1;
				}
				$.extend(settings, {
					error: function (xhr, status, index, anchor) {
						$(anchor.hash).html(
							'Couldn’t load this tab. Sorry. If this error persists, report it, please.' + currentHTML);
					},
					dataFilter: function (result/* , dataType*/) {
						switch (req) {
							case 4: // Wikitext
								result = '<pre>' + mw.html.escape(result) + '</pre>';
								break;
							case 2: // Global usage
								var bcont = $('#bodyContent', $(result)).eq(0);
								bcont.find('fieldset').hide();
								result = bcont.html();
								break;
							case 3: // Log
								var logNode = $('.mw-logline-upload', $(result)).eq(0).parent();
								result = '<ul>' + logNode.html() + '</ul>';
						}
						result = '<div style="overflow:auto;width:auto;height:' + (Math.round($(window).height() * 0.97) - 128) + 'px;background:#fff">' + result + '</div>';
						currentHTML = result;
						return result;
					}
				});
			}
		}));
		this.dlg = $('<div>').append(this.$dlgContainer).dialog({
			modal: true,
			closeOnEscape: true,
			position: 'center',
			dialogClass: 'wikiEditor-toolbar-dialog',
			title: '<a href="' + mw.util.getUrl(file) + '" target="_blank">' + mw.html.escape(file) + '</a>',
			height: Math.round($(window).height() * 0.97),
			width: Math.round($(window).width() * 0.99),
			close: function () {
				$(this).dialog('destroy');
				$(this).remove();
			}
		});
		$('.ui-dialog-content').css('overflow', 'hidden');
	},

	exportList: function () {
		var _this = this;
		mw.loader.using([
			'jquery.ui'
		], function () { _this.exportListE(); });
	},

	exportListE: function () {
	// Fix https://bugzilla.wikimedia.org/show_bug.cgi?id=32687
		if (mw.user.options.get('skin') === 'vector') {
			mw.util.addCSS(
				'.ui-buttonset .ui-button { margin-left:0!important; margin-right:-.3em!important; }\n.ui-buttonset > label.ui-state-active { background:#EEE !important; }\n'
			);
		}

		var _this = this,
			preEx = '',
			appEx = '',
			exOptions,
			$dlgNode,
			$oDiv,
			$oLabel,
			$inLabel,
			$in,
			$outLabel,
			$out,
			$prevLabel,
			$prev,
			$prevInner,
			$OTRSButton,
			$xhr,
			generateList = function (text, prepend, append) {
				if (!prepend) prepend = '';
				if (!append) append = '';
				var f,
					el,
					i = 0,
					i2 = 0,
					ltmpl = '',
					tmpl = '',
					p,
					l = 'Autolist generated by JSONListUploads aka Gallery-Tool from Wikimedia Commons, a non Wikimedia-developed tool.\n\n' + prepend;
				p = l;
				for (f in _this.uploadsRevs) {
					el = _this.uploadsRevs[f];
					if (typeof el !== 'object') continue;
					if (el.matchesFilter && el.le) {
						tmpl = text.replace(/%FULLPAGENAME%/g, el.le.title).replace(/%FULLPAGENAMEE%/g, encodeURIComponent(el.le.title))
							.replace(/%PAGENAME%/g, el.le.title.replace('File:', '')).replace(/%OTRSURL%/g, 'http://commons.wikimedia.org/wiki/' + el.le.title.replace(/ /g, '_'))
							.replace(/%TITLE%/g, el.le.title.replace('File:', '').replace(/\.[\w]{3,4}$/, ''))
							.replace(/%TIMESTAMP%/g, el.le.timestamp).replace(/%TIMESTAMP_FM%/g, _this.getDateFromMWDate(el.le.timestamp).toLocaleString())
							.replace(/%ACTION%/g, el.le.action).replace(/%ID%/g, i).replace(/%ID2%/g, i2);
						i2++;
					}
					if (el.matchesFilter && el.ii && el.le) {
						tmpl = tmpl.replace(/%COMMENT%/g, el.ii.comment).replace(/%DESCURL%/g, el.ii.descriptionurl).replace(/%URL%/g, el.ii.url)
							.replace(/%HEIGHT%/g, el.ii.height).replace(/%WIDTH%/g, el.ii.width).replace(/%SHA1%/g, el.ii.sha1).replace(/%SHA1_UP%/g, el.ii.sha1.toUpperCase()).replace(/%MIME%/g, el.ii.mime)
							.replace(/%SIZE%/g, el.ii.size).replace(/%SIZE_FM%/g, _this.secureCall('mdFormattNumber', el.ii.size >> 10) + '&nbsp;<abbr title="1 KibiByte= 1024 Bytes">KiB</abbr>');
					}
					l += tmpl;
					if (tmpl && ltmpl.length < _this.quota.maxparsesize) ltmpl += tmpl;
					tmpl = '';
					i++;
				}
				return ([(l + append), (p + ltmpl + append)]);
			},
			timeoutID = 0,
			timeoutID2 = 0,
			lastRequests = {},
			lastRequest,
			gotParserResult = function (result) {
				var $containerText = result.parse.text['*'];
				$containerText = $($containerText);
				$('.editsection', $containerText).remove();
				$('a', $containerText).attr('target', '_blank');
				lastRequests[lastRequest] = result;
				$prevInner.empty().append($containerText);
			},
			maybeGenerateList = function () {
				clearTimeout(timeoutID);
				clearTimeout(timeoutID2);
				$out.val('Please wait.');
				$prevInner.empty().text('Parsing as wikitext.');
				timeoutID = setTimeout(function () {
					var params,
						res = generateList($in.val(), preEx, appEx);
					$out.val(res[0]);
					timeoutID2 = setTimeout(function () {
						params = { format: 'json',
							action: 'parse',
							uselang: cfg.wgUserLanguage,
							prop: 'text',
							pst: true,
							text: res[1]
						};
						if ($xhr) $xhr.abort();
						lastRequest = res[1];
						if (lastRequest in lastRequests) {
							gotParserResult(lastRequests[lastRequest]);
							return;
						}
						$xhr = $.post(_this.apiURL, params, gotParserResult, 'json');
					}, 1500);
				}, 1100);
			},
			sendername = _this.user || 'NAME';
		_this.i18n.permissionLetter = _this.i18n.permissionLetter.replace(/%USER%/g, sendername)
			.replace(/%LOCATION%/g, Geo.city).replace(/%DATE%/g, new Date().toLocaleString());
		exOptions = [{ id: 'goOTRS', icon: '//upload.wikimedia.org/wikipedia/commons/6/67/Exp_OTRS.png', ltext: 'OTRS permission letter', prepend: _this.i18n.permissionLetter + '\nOTRS-Agent: Did you know you can easily use [[Help:VisualFileChange.js]] to insert permissions or make changes en masse?\n----START LIST----\n', append: '----END LIST----\nVisualFileChange.js and OTRS member\'s live is easier.', pattern: '%OTRSURL%\n' },
			{ id: 'goGallery', icon: '//upload.wikimedia.org/wikipedia/commons/1/17/Exp_Simple.png', ltext: 'Gallery (filename)', prepend: '<gallery showfilename=1>\n', append: '</gallery>', pattern: '%FULLPAGENAME%\n' },
			{ id: 'goGalleryA', icon: '//upload.wikimedia.org/wikipedia/commons/9/9b/Exp_Advanced.png', ltext: 'Advanced gallery', prepend: '<gallery caption="Gallery of ' + sendername + '" widths=400px heights=400px>\n', append: '</gallery>', pattern: '%FULLPAGENAME%|%TITLE%<br>%TIMESTAMP_FM%<br>%SIZE_FM%\n' },
			{ id: 'goGalleryB', icon: '//upload.wikimedia.org/wikipedia/commons/b/b3/Exp_Cool.png', ltext: 'Cool gallery', prepend: '<div style="text-align:center;max-width:80%;min-width:400px;margin:0 auto;">\n', append: '</div>', pattern: '<div style="width:410px; display:inline-block; -moz-border-radius: 10px; -webkit-border-radius: 10px; border-radius: 10px; -moz-box-shadow: 4px 4px 4px #CCC; -webkit-box-shadow: 4px 4px 4px #CCC; box-shadow: 4px 4px 4px #CCC;;background-color: #433; background-image: -moz-linear-gradient(top, #433, #111); background-image: -ms-linear-gradient(top, #433, #111); background-image: -o-linear-gradient(top, #433, #111); background-image: -webkit-gradient(linear, left top, left bottom, from(#433), to(#111));padding:20px;margin:10px;">[[%FULLPAGENAME%|400px]]<div style="color:#fff">%TITLE%</div></div>\n' },
			{ id: 'goStack', icon: '//upload.wikimedia.org/wikipedia/commons/a/a6/Exp_Imagestack.png', ltext: 'Manual slideshow (Imagestack)', prepend: '{{Imagestack\n|title=TITLE\n|align=left\n|loop=yes\n', append: '}}', pattern: '|%FULLPAGENAME%|%TITLE%\n' },
			{ id: 'custom', icon: '//upload.wikimedia.org/wikipedia/commons/b/bd/Exp_Custom.png', ltext: 'Custom', prepend: 'Usage: %VAR%. The following variables are available:', append: '', pattern: 'FULLPAGENAME, FULLPAGENAMEE, PAGENAME, OTRSURL, TITLE, TIMESTAMP, TIMESTAMP_FM, ACTION, COMMENT, DESCURL, URL, HEIGHT, WIDTH, SHA1, SHA1_UP, MIME, SIZE, SIZE_FM, ID, ID2\n' }
		];
		$dlgNode = $('<div>', { title: 'Export a list of files.' });
		$oDiv = $('<div>', { 'class': 'filter-frame', 'style': 'text-align:center;' });
		$oLabel = $('<label>', { 'for': '', 'text': 'Choose one of the predefined options or use custom text.' });
		$inLabel = $('<label>', { 'for': 'gExIn', 'text': 'Please specify the pattern to be used to create the output.' });
		$in = $('<textarea>', { id: 'gExIn', html: 'Pattern here', style: 'width:100%;height:130px;', rows: '5', cols: '50' }).on('input keyup', function () {
			maybeGenerateList();
		}).val('%PAGENAME% has a size of %SIZE_FM% and was uploaded on %TIMESTAMP_FM%. It\'s sha1 is %SHA1_UP%.\n');
		$outLabel = $('<label>', { 'for': 'gExOut', 'text': 'Result. For copy & paste.' });
		$out = $('<textarea>', { id: 'gExOut', html: 'Copy this code…', readonly: 'readonly', style: 'width:100%;height:130px;', rows: '5', cols: '50' }).click(function () {
			$(this).select();
		});
		$prevLabel = $('<label>', { 'for': 'gExPrev', 'text': 'Sample preview - Parsed as Wiki-text.' });
		$prev = $('<div>', { id: 'gExPrev', style: 'height:180px; overflow:visible' });
		$prevInner = $('<div>', { id: 'gExPrevInner', style: 'height:100%; width:100%; overflow:auto; background:#EFD' }).appendTo($prev);
		$.each(exOptions, function () {
			$oDiv.append($('<input name="gExOpt" type="radio">').attr('id', this.id));
			$oDiv.append($('<label>', { 'for': this.id, 'text': this.ltext, 'style': 'display:inline-block;text-align:center' })
				.append($('<br>'), $('<img>', { src: this.icon, style: 'height:80; width:auto; ', height: 80 })));
		});
		$oDiv.find('input').on('change', function () {
			var elId = $(this).attr('id');
			if (elId === 'goOTRS')
				$OTRSButton.show();
			else
				$OTRSButton.hide();

			$.each(exOptions, function () {
				if (elId === this.id) {
					preEx = this.prepend;
					appEx = this.append;
					$in.val(this.pattern);
				}
			});
			$in.keyup();
		});
		$dlgNode.text('Export a list of files that are loaded and match the filter.');
		$dlgNode.append($oLabel, $('<br>'), $oDiv.buttonset(), $('<br>'), $inLabel, $('<br>'), $in, $('<br>'), $outLabel, $('<br>'), $out, $('<br>'), $prevLabel, $('<br>'), $prev);
		// $prev.resizable(); /* Resizable breaks the size-handler of the dialog */
		$dlgNode.dialog({
			buttons: {
				'Close': function () {
					$(this).dialog('close');
				},
				'Open draft email to OTRS in your email client': function () {
					$OTRSButton.prop('disabled', true).addClass('ui-state-disabled');
					var eBody = encodeURIComponent($out.val()),
						eSubj = 'subject=' + encodeURIComponent('Permission for uploaded files - account ' + sendername) + '&body=',
						eTo = 'mailto:permissions-commons' + '@' + 'wikimedia.org';
					try {
						location.href = eTo + '?' + eSubj + eBody;
					} catch (ex) {
						try {
							location.href = eTo + '?' + eSubj + encodeURIComponent('Paste the generated text here, choose a trustworthy sender address and send the E-Mail.');
						} catch (ex2) {
							try {
								location.href = eTo;
							} catch (ex3) {
								alert('Sorry. Script was unable to open your E-Mail client. Send this Mail to ' + 'permissions-commons' + '@' + 'wikimedia.org?');
							}
						}
					}
					setTimeout(function () {
						$OTRSButton.prop('disabled', false).removeClass('ui-state-disabled');
					}, 7000);
				}
			},
			modal: true,
			width: Math.min($(window).width() * 0.98, 900),
			close: function () {
				$(this).dialog('destroy');
				$(this).remove();
			}
		});
		$OTRSButton = $('.ui-dialog-buttonpane').find('button').eq(1).hide();
		$in.keyup();
	},

	doSilentWork: function () {
		var o = this;
		if ((o.doingSilentWork) || (o.openChachedFiles > 40) || (o.quota.cycles > 60)) {
			o.$gOptions.triggerHandler('silentWorkUpdate', ['query-handler: Invoked - Canceling.']);
			return;
		}
		o.doingSilentWork = true;
		o.quota.cycles++;
		o.$gOptions.triggerHandler('silentWork');
		o.$gOptions.triggerHandler('silentWorkUpdate', ['query-handler: Running now …']);

		o.tasks = [];
		if (!this.noMoreFiles) {
			o.addTask('findUploads');
			o.addTask('queryII');
			o.addTask('traceUploads');
			o.addTask('queryII');
			o.addTask('setupGalleryBoxes');
		}
		o.addTask('silentWorkReady');
		o.nextTask();
	},

	silentWorkReady: function () {
		var o = this;
		o.doingSilentWork = false;
		o.statusTracing = false;
		o.$gOptions.triggerHandler('silentWorkUpdate', ['query-handler: Done …']);
		o.$gOptions.triggerHandler('silentWorkDone');
	},

	doAPICall: function (params, callback) {
		var o = this;

		params.format = 'json';
		o.pendingXHRs.push($.ajax({
			url: this.apiURL,
			cache: false,
			dataType: 'json',
			data: params,
			type: 'POST',
			success: function (result, status, x) {
				if (x) {
					var elPos = $.inArray(x, o.pendingXHRs);
					if (elPos !== -1) o.pendingXHRs.splice(elPos, 1);
				}

				if (!result) return o.fail('Receive empty API response:\n' + x.responseText);

				// In case we get the mysterious 231 unknown error, just try again
				if (result.error && result.error.info.indexOf('231') !== -1) {
					return setTimeout(function () {
						o.doAPICall(params, callback);
					}, 500);
				}
				// If the user does not exist, call the appropriate handler
				if (result.error && result.error.code === 'leparam_user' && result.error.info.indexOf('not found') !== -1)
					return o.secureCall('wrongUserSpecified');

				if (result.error) return o.fail('API request failed (' + result.error.code + '): ' + result.error.info);
				if (result.edit && result.edit.spamblacklist)
					return o.fail('The edit failed because ' + result.edit.spamblacklist + ' is on the Spam Blacklist');

				if (!o.christmaschecked && x && typeof x.getResponseHeader === 'function') {
					o.christmaschecked = true;
					var dat = x.getResponseHeader('date').match(/\D+(\d\d) (\D{3})/);
					if (dat && dat[1] && dat[2]) if (dat[2].toLowerCase() === 'dec' && (dat[1] > 19 < 26)) { o.christmas(); }
				}
				try {
					if (typeof o[callback] === 'function') o.secureCall(callback, result, params);
				} catch (e) {
					return o.fail(e);
				}
			},
			error: function (x, status, error) {
				if (x) {
					var elPos = $.inArray(x, o.pendingXHRs);
					if (elPos !== -1) o.pendingXHRs.splice(elPos, 1);
				} else {
					x = { status: 'xhr not implemented' };
				}
				if (x.status === 504 && o.quota.apiretry) {
				// Try it again in case of Gateway Time-out
					o.quota.apiretry--;
					return setTimeout(function () {
						o.doAPICall(params, callback);
					}, Math.round(1 / (o.quota.apiretry + 1) * 10000));
				}
				return o.fail('API request returned code ' + x.status + ' ' + status + '. Error code is ' + error);
			}
		}));
	},

	resetHash: function () {
		var oldScrollPos = $doc.scrollTop();
		document.location.hash = '';
		$('html,body').scrollTop(oldScrollPos);
	},

	doGetApiCall: function (params, callback, pCallback) {
		var o = this;

		// Local Cache
		if (params.list) {
			if (params.list === 'allusers') {
				if (params.auprefix in o.userCache) {
					o[callback](o.userCache[params.auprefix], pCallback);
					return;
				}
			} else if (params.list === 'allcategories') {
				if (params.acprefix in o.catCache) {
					o[callback](o.catCache[params.aiprefix], pCallback);
					return;
				}
			}
		}

		params.format = 'json';
		$.ajax({
			url: this.apiURL,
			cache: true,
			dataType: 'json',
			data: params,
			type: 'GET',
			async: true,
			success: function (result/* , status , x*/) {
				if (!result || !result.query) { if (typeof pCallback === 'function') pCallback(); return; }
				try {
					if (params.list) {
						if (params.list === 'allusers') {
							// cache the result
							o.userCache[params.auprefix] = result.query.allusers;
							o[callback](result.query.allusers, pCallback);
							return;
						}
					}
					if (params.list) {
						if (params.list === 'allcategories') {
							// cache the result
							o.catCache[params.aiprefix] = result.query.allcategories;
							o[callback](result.query.allcategories, pCallback);
							return;
						}
					}
					// This is a "must", the doc sais
					if (typeof pCallback === 'function') pCallback();
					o[callback](result);
				} catch (e) {
					return o.fail(e);
				}
			},
			error: function () {
			// This is a "must", the doc sais
				if (typeof pCallback === 'function') pCallback();
			}
		});
	},

	autoErrorReport: function () {
		var reportPage = 'Commons:Gallery tool/AutoErrors',
			page = {
				action: 'edit',
				title: reportPage,
				token: this.edittoken,
				summary: 'Reporting JavaScript Gallery-Tool Error',
				watchlist: 'nochange',
				appendtext: '\n----\n' + $.trim(this.errorReportText + ' ~~' + '~~ \n\n----'),
				redirect: 1
			};
		this.doAPICall(page, 'autoErrorReportCB');
	},

	autoErrorReportCB: function () {
		alert('Thank you for reporting this bug. Your report has been saved. We regret the inconvenience.');
		$('#errorNode').remove();
	},

	fail: function (err) {
		if (typeof err === 'object') {
			var stErr = mw.html.escape(err.message) + '<br>' + err.name;
			if (err.lineNumber) stErr += ' @line' + err.lineNumber;
			if (err.line) stErr += ' @line' + err.line;
			err = stErr;
		} else {
			err = mw.html.escape(err.toString());
		}
		/* if ( window.console !== undefined && $.isFunction( window.console.log ) )
		console.log('--------- > Error: ' + err.toString() + ' @' + this.currentTask + '|after:' + this.oldTask + '\nSTACK:' + err.stack);*/
		this.errorReportText = $.trim(err.toString()) + '@' + this.currentTask + '\n\n' + 'Client: ' + $.client.profile().name + ' ' +
			$.client.profile().version + '\n\nSettings: user:' + mw.html.escape(this.user) + '\n\n' + this.i18n.version + this.mdScriptRevision;
		this.$errorNode.append('During the execution of JSONListUploads, the following error occured: ' + this.errorReportText + '<br><br>');
		this.$errorNode.fadeIn();
		this.$gOptions.triggerHandler('unLockUI');
	},

	/**
	 * Method to catch errors and report where they occured
	 */
	secureCall: function (stFunction) {
	// console.log('Calling: ' + stFunction + ' ' + typeof this[stFunction]);
		if (!stFunction) return;
		var o = this;
		o.oldTask = o.currentTask;
		try {
			o.currentTask = stFunction;
			// Argument doesn't inherit Array, but is Array-like and thus compatible
			// with Array methods. Use slice() to strip the stFunction parameter.
			var args = Array.prototype.slice.call(arguments, 1),
				res = o[stFunction].apply(o, args);
			o.currentTask = o.oldTask;
			return res;
		} catch (ex) {
			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) {
		this.tasks.push(task);
	},
	nextTask: function () {
		var task = this.currentTask = this.tasks.shift();
		return ($.isArray(task) ? this.secureCall.apply(this, task) : this.secureCall(task)); // Ja da guckste …
	},
	lastTask: function () {
		var task = this.currentTask = this.tasks[this.tasks.length - 1];
		this.tasks = [];
		return ($.isArray(task) ? this.secureCall.apply(this, task) : this.secureCall(task));
	},

	mdSelfPath: 'MediaWiki:JSONListUploads.js',
	mdErrReportPath: 'MediaWiki_talk:JSONListUploads.js', // User talk:Rillke/JSONListUploads.js

	mdUserTalkPrefix: cfg.wgFormattedNamespaces[3] + ':',
	mdContribPrefix: cfg.wgFormattedNamespaces[-1] + ':Contributions/',
	mdReLoader: '<p class="center"><img src="//upload.wikimedia.org/wikipedia/commons/c/ce/RE_Ajax-Loader.gif"/></p>',
	helpHover: $('<span>', { style: 'color:#22D; background-color: #F0FAFE; padding: 0.0em 0.4em; font-size: 80%; text-align: center; -moz-border-radius: 3px; -webkit-border-radius: 3px; border-radius: 3px; border:solid 1px #89D7F9' }).append('?'),
	apiURL: mw.util.wikiScript('api'),

	uActions: {
		upload: cThumb + 'b/b5/Gnome-document-new.svg/24px-Gnome-document-new.svg.png',
		overwrite: cThumb + 'c/c7/File_move_icon.svg/27px-File_move_icon.svg.png',
		revert: cThumb + 'e/e9/Gnome-document-open-recent.svg/24px-Gnome-document-open-recent.svg.png'
	},

	// Translatable strings
	i18n: {
		'filelist': 'Filelist.',
		'version': 'Version: ',
		'help': 'General help via Commons:Help Desk.',
		'faq': 'FAQ and known issues of this tool.',
		'export': 'Export to gallery: Export the list of files currently matching the filter and loaded.',
		'change': 'Change user, start date, sorting, view',
		'start': ' &nbsp;Start&nbsp;at&nbsp;',
		'invalidD': 'Invalid. Use YYYY-MM-DD',
		'sorting': ' &nbsp;Sorting&nbsp;',
		'newest': 'Newest',
		'oldest': 'Oldest',
		'submit': 'submit',
		'vdetails': 'Visible details',
		'hdetails': 'Details on hover',
		'filtering': 'Filtering options',
		'created': '&nbsp;Created&nbsp; ',
		'overwrote': '&nbsp;Overwrote&nbsp; ',
		'reverted': '&nbsp;Reverted&nbsp; ',
		'apply': 'apply',
		'deleted': 'Show&nbsp;Deleted',
		'latest': 'Show only the user’s latest upload per file',
		'state': 'select state – top/old',
		'top': 'top – upload is current version of file',
		'old': 'old – not current version of file',
		'page': 'Page: ',

		'optUser': 'The user whose contributions are to be shown',
		'optStartAt': 'Start listing from a specific date',
		'optStartAtHowTo': 'Format:  YYYY-MM-DD<br> or, optionally<br>YYYY-MM-DD&nbsp;hh:mm:ss<br>or use the picker',
		'optIntit': 'Files the user created new on Commons',
		'optOverwrite': 'Files where the user overwrote an earlier version of the file',
		'optRevert': 'Files the user reverted to an earlier version',
		'optDeleted': 'Files which have been deleted',
		'optTop': 'Filter by state of a file-revision. Old implies that the file has been overwritten',
		'optHideRevs': 'Unselect to show all revisions the user uploaded',
		'optCat': 'Filter by category',
		'optCatHowTo': 'Fill in the category and hit enter.\nRemove your input when you do not want to filter by category',
		'inCatPlaceholder': 'In category',
		'gPagesPanel': 'Pages control panel. By design, it is undeterminable for the script how many uploads a user has before loading a complete list of them; however you can step through all uploads by clicking >>',
		'statResultHelp': 'These are pure log-statistics of the user. Files may be deleted inbetween, or the revision history – shown on a file-description-page – has been manipulated by an administrator',
		'filterHelp': 'To improve the performance, not all files are loaded. If you filter, you will probably get fewer results than you expect. To get all results, click on >> until a statistics-box shows up, then click on apply',
		'filterHelp2': 'All files are loaded now so the filter-results are right',
		'permissionLetter': 'Correct the terms in [] if necessary.\nTo: permissions-commons ät wikimedia.org\n\nI hereby affirm that I, [%USER%] am the creator and/or sole owner of the exclusive copyright of the files uploaded to Wikimedia Commons, listed below.\n\
		I agree to publish that work under the free license "Creative Commons Attribution-ShareAlike 3.0" (unported) and GNU Free Documentation License (unversioned, with no invariant sections, front-cover texts, or back-cover texts).\n\n\
		I acknowledge that by doing so I grant anyone the right to use the work in a commercial product or otherwise, and to modify it according to their needs, provided that they abide by the terms of the license and any other applicable laws.\n\n\
		I am aware that this agreement is not limited to Wikipedia or related sites.\n\n\
		I am aware that I always retain copyright of my work, and retain the right to be attributed in accordance with the license chosen. Modifications others make to the work will not be claimed to have been made by me.\n\n\
		I am aware that the free license only concerns copyright, and I reserve the option to take action against anyone who uses this work in a libelous way, or in violation of personality rights, trademark restrictions, etc.\n\n\
		I acknowledge that I cannot withdraw this agreement, and that the work may or may not be kept permanently on a Wikimedia project.\n\n\
		[%LOCATION%], [%DATE%] [%USER%] ([SENDER\'S AUTHORITY (Are you the copyright-holder, director, appointed representative of, etc.)])\n\n'
	},

	botIgnorance: { Rotatebot: '', Cropbot: '', FlickreviewR: '' }, // easyer to query: "cadidate" in botIgnorance

	mdTagRecognization: [
	// RegExp for the tag								note to add to the thumb-page
		[/Deletion requests.*/,								'd'],
		[/Incomplete deletion requests.*/,					'd(incomplete)'],
		[/Media missing permission.*/,						'!p'],
		[/Media without a license.*/,						'!l'],
		[/Media uploaded without a license.*/,				'uwl'],
		[/Media without a source.*/,						'!s'],
		[/Other speedy deletions.*/,						'speedyd'],
		[/Copyright violations.*/,							'copyvio'],
		[/OTRS pending.*/,									'on', 'ddf'],
		[/Items with OTRS permission confirmed.*/,			'opm', 'bcf'],
		[/OTRS received.*/,									'or', 'cdf'],
		[/[Aa]dmin[- ]reviewed [licenses|Flickr images]/,	'lrd', '8fa'],
		[/Flickr images reviewed by/,						'frd', '8fa'],
		[/Flickr images needing human review/,				'frR', 'af8'],
		[/License review needed/,							'lrR', 'af8']
	],

	mdLicenseRecognization: [
	// 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, 								'LibreA'],
		[/Category:Images requiring attribution/i, 		'Attrib'],
		[/Category:Copyrighted free use.*/i, 			'CFreeUse'],
		[/Category:Mozilla Public License/i, 			'MPL'],
		[/Category:GPL/i, 								'GPL'],
		[/Category:Free screenshot.*/i, 				'free-Screenshot'],
		[/Category:Copyright by Wikimedia.*/i, 			'(c)WMF'],
		[/Category:Self[- _]published[ß\- _]work/i, 	'<b>self</b>'], // rm the next tags from being shown
		[/Valid SVG/, ''], [/Translation possible/, ''], [/License[- _]migration redundant/, ''], [/Retouched pictures/, ''], [/Extracted images/, ''], [/Images with annotations/, ''], [/Media needing category/, ''], [/requiring review/, ''], [/[Flickr|License] review needed/, ''], [/[Aa]dmin[- ]reviewed/, ''], [/Flickr images reviewed by/, ''], [/UploadWizard/, ''], [/OTRS[- _]received/, ''], [/Items[- _]with[- _]OTRS[- _]permission/, ''], [/OTRS[- _]pending/, ''], [/Media with locations/, ''], [/Media missing/, ''], [/Media without a/, ''], [/Deletion requests/, '']
	]
};

mw.loader.using([
	'ext.gadget.tipsyDeprecated',
	'ext.gadget.jquery.badge',
	'jquery.ui',
	'jquery.ui',
	'jquery.ui',
	'jquery.ui',
	'mediawiki.cookie',
	'jquery.client',
	'jquery.spinner',
	'mediawiki.util',
	'mediawiki.user',
	'mediawiki.language',
	'mediawiki.page.gallery.styles',
	'user'
], function () { JLU.preinstall(); });

}(jQuery)); // </nowiki>