User:Evad37/Covery.js

Source: Wikipedia, the free encyclopedia.
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
/* jshint esversion: 6, laxbreak: true, undef: true, eqnull: true, maxerr: 999 */
/* globals console, document, File, FileReader, fetch, window, $, mw, OO */
// <nowiki>
var setupCovery = function setupCovery() {
	var SCRIPT = {
		name: 'Covery',
		version: '1.5.0',
		ad: ' (using [[User:Evad37/Covery|Covery]])'
	};

	function isSuitable() {
		var config = mw.config.get(['wgAction', 'wgDiffOldId', 'wgNamespaceNumber', 'wgPageName']);
		if (
			config.wgAction !== 'view' ||
			config.wgNamespaceNumber !== 0 ||
			config.wgDiffOldId !== null
		) {
			return $.Deferred().reject();
		}
		return config;
	}

	var getLeadWikitext = function getLeadWikitext(api, pageName) {
		return api
			.get({
				action: 'parse',
				format: 'json',
				page: pageName,
				prop: 'wikitext',
				section: '0'
			})
			.then(function(response) {
				return response.parse.wikitext['*'];
			});
	};

	/**
	 *
	 * @param {String} wikitext parameters section from a template, including pipes before each parameter name inside braces `{{...}}`
	 */
	var getTemplateParameters = function getTemplateParameters(wikitext) {
		var params = {};
		var unnamedParamCount = 0;
		var templateParamsPattern = /\|(?!(?:[^{]+}|[^\[]+]))(?:.|\s)*?(?=(?:\||$)(?!(?:[^{]+}|[^\[]+])))/g;
		var parts = wikitext.match(templateParamsPattern);
		return parts.map(function(part, position) {
			var isEmptyParameter = part.trim() === '|'; //  i.e. first parameter of {{foo||bar}
			if (isEmptyParameter) {
				unnamedParamCount++;
				return {
					name: unnamedParamCount.toString(),
					value: '',
					wikitext: {
						name: '|',
						value: part.slice(1)
					}
				};
			}
			var equalsIndex = part.indexOf('=');
			var bracesIndex = part.indexOf('{{');

			var hasNoEqualsSign = equalsIndex === -1;
			var firstEqualsSignWithinBraces = bracesIndex !== -1 && bracesIndex < equalsIndex;
			var isUnnamedParameter = hasNoEqualsSign || firstEqualsSignWithinBraces;
			if (isUnnamedParameter) {
				unnamedParamCount++;
				return {
					name: unnamedParamCount.toString(),
					value: part.slice(1).trim(),
					wikitext: {
						name: '|',
						value: part.slice(1)
					}
				};
			} else {
				return {
					name: part.slice(1, equalsIndex).trim(),
					value: part.slice(equalsIndex + 1).trim(),
					wikitext: {
						name: part.slice(0, equalsIndex + 1),
						value: part.slice(equalsIndex + 1)
					}
				};
			}
		});
	};

	var getInfoboxTemplate = function getInfoboxTemplate(wikitext) {
		var infoboxPattern = /\{\{\s*(.*?[Ii]nfobox.*?|[Ii]OS App)\s*(\|(?:.|\n)*?(?:(?:\{\{(?:.|\n)*?(?:(?:\{\{(?:.|\n)*?\}\})(?:.|\n)*?)*?\}\})(?:.|\n)*?)*|)\}\}\n?/;
		var infoboxParts = infoboxPattern.exec(wikitext);
		if (!infoboxParts || !infoboxParts[0] || !infoboxParts[1]) {
			throw new Error('Unable to parse infobox from wikitext `' + wikitext + '`');
		}
		var name = infoboxParts[1];
		var params = infoboxParts[2] ? getTemplateParameters(infoboxParts[2]) : [];
		return {
			name: name,
			params: params,
			wikitext: infoboxParts[0]
		};
	};

	var toSentanceCase = function toSentanceCase(text) {
		return text.slice(0, 1).toUpperCase() + text.slice(1);
	};

	var checkInfobox = function checkInfobox(infobox) {
		var videoGameInfoboxTemplates = [
			'Infobox video game',
			'Infobox Arcade Game',
			'Infobox Videogame',
			'Infobox cvg',
			'Infobox Arcade game',
			'Infobox arcade',
			'GameInfobox',
			'Infobox CVG',
			'Infobox vg',
			'Infobox Video Game',
			'Infobox VG',
			'IOS App',
			'Infobox video games',
			'Vg infobox',
			'Infobox videogame',
			'Infobox console game',
			'Infobox computer game',
			'Videogame infobox',
			'Video game infobox',
			'Infobox video game series',
			'Infobox VG series',
			'Infobox video game franchise'
		];
		var infoboxName = toSentanceCase(infobox.name);
		if (!videoGameInfoboxTemplates.includes(infoboxName)) {
			throw new Error('{{Infobox video game}} not found.');
		}
		var imageParam = infobox.params.find(paramByName('image'));
		if (imageParam && imageParam.value) {
			throw new Error('Infobox already has an image!');
		}
		return infobox;
	};

	/**
	 * @param {File} file source file
	 * @param {Number} maxResolution maximum resolution in pixels
	 * @returns {Promise} Promise of (1) a File with the given max resoltion, and (2) a data url of the resized image
	 **/
	var resizeImageFile = function resizeImageFile(file, maxResolution) {
		var resizeFilePromise = $.Deferred();

		var origImg = document.createElement('img');

		var reader = new FileReader();
		reader.onload = function(e) {
			origImg.addEventListener(
				'load',
				function() {
					var canvas = document.createElement('canvas');
					var ctx = canvas.getContext('2d');
					ctx.drawImage(origImg, 0, 0);
					var resolution = origImg.width * origImg.height;
					var scaleFactor =
						resolution > maxResolution ? Math.sqrt(maxResolution / resolution) : 1;
					var width = origImg.width * scaleFactor;
					var height = origImg.height * scaleFactor;
					canvas.width = width;
					canvas.height = height;
					ctx = canvas.getContext('2d');
					ctx.drawImage(origImg, 0, 0, width, height);

					var dataurl = canvas.toDataURL(file.type);

					canvas.toBlob(function(blob) {
						resizeFilePromise.resolve(
							new File([blob], file.name, { type: file.type }),
							dataurl
						);
					}, file.type);
				},
				false
			);
			origImg.src = e.target.result;
		};
		reader.readAsDataURL(file);

		return resizeFilePromise.promise();
	};

	/**
	 *
	 * @param {String} articleTitle
	 * @param {String} developer
	 * @param {String} publisher
	 * @param {String[]} platforms
	 */
	var makeDescriptionText = function makeDescriptionText(
		articleTitle,
		developer,
		publisher,
		platforms
	) {
		var platformsParams = platforms.reduce(function(params, platform) {
			return params + '|' + platform;
		}, '');
		return (
			'==Summary==\n{{Non-free use rationale video game cover\n' +
			'| Article = ' +
			articleTitle.getPrefixedText() +
			'\n' +
			'| Use = Infobox\n' +
			'| Publisher = ' +
			publisher +
			'\n' +
			'| Developer = ' +
			developer +
			'\n}}\n' +
			'==Licensing==\n{{Non-free video game cover' +
			platformsParams +
			'}}'
		);
	};

	/**
	 * @param {Object} api
	 * @param {File} file
	 * @param {String} text wikitext for the file description page
	 * @param {Object} title mw.Title object
	 * @returns {Promise} Promise of result object, or an error code and a jqxhr object
	 */
	var uploadFile = function uploadFile(api, file, text, title) {
		var filename = title.getMain();
		return api
			.postWithToken(
				'csrf',
				{
					action: 'upload',
					format: 'json',
					filename: filename,
					comment: 'Upload cover image' + SCRIPT.ad,
					text: text,
					file: file
				},
				{ contentType: 'multipart/form-data' }
			)
			.then(function(response) {
				/* on success, will get an object like:
            { upload:
                filename: "Image_page_sandbox_1000x596.png",
                imageinfo: {
                    bitdepth: 8,
                    canonicaltitle: "File:Image page sandbox 1000x596.png",
                    ...
                },
                result: "Success"
            }
            */
				if (response && response.upload && response.upload.result === 'Success') {
					return true;
				}
				return $.Deferred().reject('API failed to upload file');
			});
	};

	var createFileTalkpage = function(api, fileTitle) {
		return api.postWithToken('csrf', {
			action: 'edit',
			format: 'json',
			title: fileTitle.getTalkPage().toString(),
			text: '{{WikiProject Video games}}',
			summary: 'WikiProject tagging (using [[User:Evad37/Covery|Covery]])',
			createonly: true
		});
	};

	/**
	 * @param {String} pageTitle
	 * @returns {Promise} {wikitext: {String} Revision wikitext, timestamp: {String} last edit timestamp}
	 */
	var getRevisionWikitext = function getRevisionWikitext(api, pageTitle) {
		return api
			.get({
				action: 'query',
				format: 'json',
				prop: 'revisions',
				titles: pageTitle,
				rvprop: 'timestamp|content',
				rvslots: 'main'
			})
			.then(function(response) {
				return $.map(response.query.pages, function(page) {
					return {
						wikitext: page.revisions[0].slots.main['*'],
						timestamp: page.revisions[0].timestamp
					};
				})[0];
			});
	};

	var paramByName = function paramByName(name) {
		return function(param) {
			return param.name === name;
		};
	};

	var makeInfoboxWikitext = function makeInfoboxWikitext(originalInfobox, newParameters) {
		var updatedParametersWikitext = originalInfobox.params.map(function(param) {
			var updatedParam = newParameters.find(paramByName(param.name));
			return (
				param.wikitext.name +
				(updatedParam ? ' ' + updatedParam.value + '\n' : param.wikitext.value)
			);
		});
		var originalParametersList = originalInfobox.params.map(function(param) {
			return param.name;
		});
		var parametersToAddWikitext = newParameters
			.filter(function(param) {
				return !originalParametersList.includes(param.name);
			})
			.map(function(param) {
				return '|' + param.name + ' = ' + param.value + '\n';
			});

		return (
			'{{' +
			originalInfobox.name +
			'\n' +
			updatedParametersWikitext.join('') +
			parametersToAddWikitext.join('') +
			'}}'
		);
	};

	var updateWikitext = function(revisionWikitext, infobox, updatedParams) {
		if (revisionWikitext.indexOf(infobox.wikitext) === -1) {
			return $.Deferred().reject('Edit conflict');
		}
		var newInfobox = makeInfoboxWikitext(infobox, updatedParams);
		return revisionWikitext.replace(infobox.wikitext, newInfobox);
	};

	var editPage = function(api, pageTitle, wikitext, timestamp) {
		return api.postWithToken('csrf', {
			action: 'edit',
			title: pageTitle,
			text: wikitext,
			summary: 'Added cover image (using [[User:Evad37/Covery|Covery]])',
			basetimestamp: timestamp,
			nocreate: true
		});
	};

	var updatePage = function updatePage(api, page, infobox, updatedParams) {
		return getRevisionWikitext(api, page)
			.then(function(revision) {
				return $.when(
					updateWikitext(revision.wikitext, infobox, updatedParams),
					revision.timestamp
				);
			})
			.then(function(updatedWikitext, timestamp) {
				return editPage(api, page, updatedWikitext, timestamp);
			});
	};

	var updateTalkpageWikitext = function updateTalkpageWikitext(revisionWikitext) {
		/* Redirects to {{WikiProject Video games}} :
        //    Template:Cvgproj',
        //    Template:WikiProject Video Games',
        //    Template:WPVG',
        //    Template:Vgproj',
        //    Template:Wpvg',
        //    Template:WP video games',
        //    Template:WP cvg',
        //    Template:WikiProject Rockstar Games',
        //    Template:WGVG',
        //    Template:WP Video games',
        //    Template:WikiProject VG',
        //    Template:WikiProject video games (redirect page)
        */
		var bannerPattern = /\{\{\s*([Ww](?:P|p|G|ikiProject) ?c?[Vv](?:ideo )?[Gg](?:ames)?|[Cc]?[Vv]gproj|[Ww]ikiProject Rockstar Games)\s*(\|(?:.|\n)*?(?:(?:\{\{(?:.|\n)*?(?:(?:\{\{(?:.|\n)*?\}\})(?:.|\n)*?)*?\}\})(?:.|\n)*?)*|)\}\}\n?/;
		var banner = bannerPattern.exec(revisionWikitext);
		var noBannerPrersent = !banner || !banner[0];
		if (noBannerPrersent) {
			return '{{WikiProject Video games}}\n' + revisionWikitext;
		}
		var noParamsInBanner = !banner[2];
		if (noParamsInBanner) {
			return false;
		}
		var params = getTemplateParameters(banner[2]);
		var coverParam = getTemplateParameters(banner[2]).find(paramByName('cover'));
		if (!coverParam) {
			return false;
		}
		var updatedBannerWikitext = banner[0].replace(
			coverParam.wikitext.name + coverParam.wikitext.value,
			''
		);
		return revisionWikitext.replace(banner[0], updatedBannerWikitext);
	};

	var updateTalkpage = function updateTalkpage(api, page) {
		var talkpageTitle = mw.Title.newFromText(page).getTalkPage();
		var talkpage = talkpageTitle && talkpageTitle.toString();
		return getRevisionWikitext(api, talkpage)
			.then(function(revision) {
				return $.when(updateTalkpageWikitext(revision.wikitext), revision.timestamp);
			})
			.then(function(updatedWikitext, timestamp) {
				if (!updatedWikitext) {
					return 'Done';
				}
				return editPage(api, talkpage, updatedWikitext, timestamp);
			});
	};

	var CoveryDialog = function CoveryDialog(config) {
		CoveryDialog.super.call(this, config);
	};
	OO.inheritClass(CoveryDialog, OO.ui.ProcessDialog);

	CoveryDialog.static.name = 'coveryDialog';
	CoveryDialog.static.title = 'Covery';
	CoveryDialog.static.size = 'large';
	CoveryDialog.static.actions = [
		{ flags: ['primary', 'progressive'], label: 'Upload', action: 'upload' },
		{ flags: 'safe', label: 'Cancel' }
	];

	// Customize the initialize() function to add content and layouts:
	CoveryDialog.prototype.initialize = function() {
		CoveryDialog.super.prototype.initialize.call(this);

		this.panel = new OO.ui.PanelLayout({ padded: true, expanded: false });

		/* Form content: */
		this.content = new OO.ui.FieldsetLayout();

		this.fileSelect = new OO.ui.SelectFileWidget({
			droppable: true,
			showDropTarget: true,
			// thumbnailSizeLimit: 0,
			$element: $("<div style='background: #eee;'>")
		});
		// this.fileSelect.$element
		// 	.find(".oo-ui-selectFileWidget-dropTarget").css({"height":"auto"});
		// this.fileSelect.$element
		// 	.find(".oo-ui-selectFileWidget-thumbnail").css({"display":"none"});
		// this.fileSelect.$element
		// 	.find(".oo-ui-selectFileInputWidget-info").css({"margin":"0"});
		
		this.urlInput = new OO.ui.TextInputWidget({
			type: 'url',
			placeholder: 'http://'
		});
		this.imagePreview = new OO.ui.LabelWidget({ label: '...' });
		this.titleInput = new OO.ui.TextInputWidget({ required: true });
		this.captionInput = new OO.ui.TextInputWidget();
		this.altTextInput = new OO.ui.TextInputWidget();
		this.developerInput = new OO.ui.TextInputWidget({ required: true });
		this.publisherInput = new OO.ui.TextInputWidget({ required: true });
		this.platformInput = new OO.ui.MenuTagMultiselectWidget({
			inputPosition: 'inline',
			allowDisplayInvalidTags: true,
			allowArbitrary: true
		});

		this.fileSelectField = new OO.ui.FieldLayout(this.fileSelect, {
			label: 'Upload a file...',
			align: 'top'
		});
		this.fileSelect.field = this.fileSelectField;
		this.urlInputField = new OO.ui.FieldLayout(this.urlInput, {
			label: '...or enter a URL',
			align: 'left'
		});
		this.urlInput.field = this.urlInputField;
		this.imagePreviewField = new OO.ui.FieldLayout(this.imagePreview, {
			label: 'Preview:',
			align: 'top'
		});
		this.titleInputField = new OO.ui.FieldLayout(this.titleInput, {
			label: 'File name',
			align: 'top'
		});
		this.titleInputField.$element
			.find(".oo-ui-fieldLayout-messages").css({"margin-top":"2em"}); // prevent errors overlapping input
		this.captionInputField = new OO.ui.FieldLayout(this.captionInput, {
			label: 'Caption',
			align: 'left'
		});
		this.altTextInputField = new OO.ui.FieldLayout(this.altTextInput, {
			label: 'Alt text',
			align: 'left'
		});
		this.developerInputField = new OO.ui.FieldLayout(this.developerInput, {
			label: 'Developer',
			align: 'left'
		});
		this.publisherInputField = new OO.ui.FieldLayout(this.publisherInput, {
			label: 'Publisher',
			align: 'left'
		});
		this.platformInputField = new OO.ui.FieldLayout(this.platformInput, {
			label: 'Platform(s)',
			align: 'left'
		});

		this.content.addItems([
			this.fileSelectField,
			// this.urlInputField, 
			this.titleInputField,
			//this.imagePreviewField,
			this.captionInputField,
			this.altTextInputField,
			this.developerInputField,
			this.publisherInputField,
			this.platformInputField
		]);

		/* Progress status content: */
		this.progressStatusContent = new OO.ui.FieldsetLayout({
			label: 'Status'
		});
		this.progressBar = new OO.ui.ProgressBarWidget({
			progress: 0
		});
		this.progressField = new OO.ui.FieldLayout(this.progressBar, {
			label: '',
			align: 'below'
		});
		this.progressStatusContent.addItems([this.progressField]);
		this.progressStatusContent.toggle(false); //hide

		this.panel.$element.append([this.content.$element, this.progressStatusContent.$element]);
		this.$body.append(this.panel.$element);

		this.fileSelect.connect(
			this,
			{ change: 'onFileSelectChange' }
		);
		this.urlInput.connect(
			this,
			{ change: 'onUrlInputChange' }
		);
		this.titleInput.connect(
			this,
			{ flag: 'onTitleInputFlag' }
		);
		this.developerInput.connect(
			this,
			{ change: 'onRequiredInputChange' }
		);
		this.publisherInput.connect(
			this,
			{ change: 'onRequiredInputChange' }
		);
		this.platformInput.connect(
			this,
			{ add: 'onPlatformInputAdd' }
		);
		(function(self) {
			self.platformInput.$element.find('input').on('blur', function() {
				self.onPlatformInputBlur.call(self);
			});
		})(this);
	};

	CoveryDialog.prototype.onFileChosen = function(filePromise, fileName, widgetUsed, otherWidget) {
		widgetUsed.pushPending();
		widgetUsed.field.setErrors([]);
		otherWidget.setDisabled(true);
		var self = this;
		$.when(filePromise)
			.then(function(file) {
				return resizeImageFile(file, 100000);
			})
			.then(
				function(resizedFile, resizedDataURL) {
					self.resizedFile = resizedFile;
					self.imagePreview.$element
						.empty()
						.show()
						.append($('<img>').attr('src', resizedDataURL));
					self.updateSize();
					widgetUsed.popPending();
					if (widgetUsed.setIndicator) {
						widgetUsed.setIndicator('required');
					}
					otherWidget.setDisabled(false);
					if (otherWidget.setIndicator) {
						otherWidget.setIndicator(null);
					}
					self.titleInput.setValue(fileName);
					self.onRequiredInputChange();
				},
				function(code) {
					var errorMessage = code
						? 'An error occured: ' + code
						: 'An unexpected error occured';
					self.resizedFile = null;
					widgetUsed.popPending();
					if (widgetUsed.setIndicator) {
						widgetUsed.setIndicator('clear');
					}
					widgetUsed.field.setErrors([errorMessage]);
					otherWidget.setDisabled(false);
					if (otherWidget.setIndicator) {
						otherWidget.setIndicator(null);
					}
					self.onRequiredInputChange();
				}
			);
	};

	CoveryDialog.prototype.onFileSelectChange = function(files) {
		var file = files && files[0];
		if (!file || !file.name) {
			return;
		}
		this.onFileChosen(file, file.name, this.fileSelect, this.urlInput);
	};

	CoveryDialog.prototype.onUrlInputChange = function(value) {
		if (!value) {
			this.urlInput.setIcon(null);
			return;
		}
		var hasImageExtension = /\.(?:gif|png|jpe?g|svg|tiff?)$/i.test(value);
		if (!hasImageExtension) {
			this.urlInput.setIcon('ellipsis');
			return;
		}
		var filePromise = fetch(value, {mode: 'no-cors'}).then(function(result) {
			return result.blob();
		});
		var fileName = value.replace(/^.*\//, '');
		this.onFileChosen(filePromise, fileName, this.urlInput, this.fileSelect);
	};

	CoveryDialog.prototype.onTitleInputFlag = function(flag) {
		if (flag.invalid === true) {
			if (this.titleInput.getValue().length) {
				this.titleInputField.setErrors(['Invalid file name']);
			}
			this.actions.setAbilities({
				upload: false
			});
		} else {
			this.onRequiredInputChange();
		}
	};

	CoveryDialog.prototype.checkMimes = function() {
		var mimeLookup = {
			'.bmp': 'image/bmp',
			'.gif': 'image/gif',
			'.jpeg': 'image/jpeg',
			'.jpg': 'image/jpeg',
			'.png': 'image/png',
			'.svg': 'image/svg+xml',
			'.tif': 'image/tiff',
			'.tiff': 'image/tiff'
		};
		var fileMime = (this.resizedFile && this.resizedFile.type) || '';
		var titleParts = this.titleInput
			.getValue()
			.toLowerCase()
			.match(/.*(\..*)$/, '$1');
		var titleExtension = titleParts && titleParts[1];
		var impliedTitleMime = mimeLookup[titleExtension] || '';
		return fileMime === impliedTitleMime;
	};

	// Only allow uploading if requirements are met
	CoveryDialog.prototype.onRequiredInputChange = function(change) {
		var self = this;
		$.when((change && change.titleIsValid) || this.titleInput.getValidity()).then(
			function() {
				// remove any old title input errors
				self.titleInputField.setErrors([]);
				// check file mime matches title mime
				var titleHasCorrectExtension = self.checkMimes();
				if (!titleHasCorrectExtension && self.resizedFile) {
					self.titleInputField.setErrors([
						'Invalid file extension (file is a ' +
							self.resizedFile.type
								.replace('image/', '')
								.replace(/\+.*$/, '')
								.toUpperCase() +
							' image)'
					]);
				}

				var requirementsMet =
					!self.fileSelect.isPending() &&
					!self.urlInput.isPending() &&
					!!self.resizedFile &&
					!!self.titleInput.getValue().length &&
					titleHasCorrectExtension &&
					!!self.developerInput.getValue().length &&
					!!self.publisherInput.getValue().length;
				self.actions.setAbilities({
					upload: requirementsMet
				});
			},
			function() {
				if (self.titleInput.getValue().length) {
					self.titleInputField.setErrors(['Invalid file name']);
				}
				self.actions.setAbilities({
					upload: false
				});
			}
		);
	};

	CoveryDialog.prototype.onPlatformInputAdd = function(item) {
		this.api
			.get({
				action: 'query',
				format: 'json',
				titles: 'Category:' + item.data + ' game covers'
			})
			.then(function(response) {
				return $.map(response.query.pages, function(page) {
					return page.missing !== '';
				})[0];
			})
			.then(function(isValid) {
				item.toggleValid(isValid);
			});
	};

	CoveryDialog.prototype.onPlatformInputBlur = function() {
		this.platformInput.doInputEnter();
	};

	// Specify the dialog height (or don't to use the automatically generated height).
	CoveryDialog.prototype.getBodyHeight = function() {
		return this.panel.$element.outerHeight(true);
	};

	// Set up the window with data passed to it at the time of opening.
	CoveryDialog.prototype.getSetupProcess = function(data) {
		data = data || {};
		return CoveryDialog.super.prototype.getSetupProcess.call(this, data).next(function() {
			this.uploaded = false;
			this.createdFileTalkpage = false;
			this.updatedArticle = false;

			this.api = data.api;
			this.infobox = data.infobox;
			this.pageName = data.pageName;
			var developerParam = data.infobox.params.find(paramByName('developer'));
			var publisherParam = data.infobox.params.find(paramByName('publisher'));
			this.developerInput.setValue((developerParam && developerParam.value) || '');
			this.publisherInput.setValue((publisherParam && publisherParam.value) || '');
			this.titleInput.setValidation(function(value) {
				var title = mw.Title.newFromFileName(value);
				if (title === null) {
					return false;
				}
				return data.api
					.get({
						action: 'query',
						format: 'json',
						prop: 'imageinfo',
						titles: title.toString(),
						iiprop: ''
					})
					.then(function(response) {
						return $.map(response.query.pages, function(page) {
							return page.missing === '' && page.imagerepository === '';
						})[0];
					});
			});
			var self = this;
			data.api
				.get({
					action: 'query',
					format: 'json',
					list: 'categorymembers',
					cmtitle: 'Category:Video game covers',
					cmprop: 'title',
					cmtype: 'subcat',
					cmlimit: 'max'
				})
				.then(function(response) {
					return response.query.categorymembers
						.map(function(category) {
							return category.title;
						})
						.map(function(categoryTitle) {
							return {
								data: categoryTitle.replace(/Category\:(.+) game covers/, '$1')
							};
						});
				})
				.then(function(platforms) {
					self.platformInput.addOptions(platforms);
				});
		}, this);
	};

	CoveryDialog.prototype.setProgressStatus = function(label, progress) {
		this.progressBar.setProgress(progress);
		this.progressField.setLabel(label);
	};

	CoveryDialog.prototype.setProgressError = function(label, progress) {
		this.getActions().forEach(null, function(actionWidget) {
			if (actionWidget.getAction() === 'upload') {
				actionWidget.setLabel('Retry');
			}
			actionWidget.setDisabled(false);
		});
		this.setProgressStatus(label, progress);
	};

	// Specify processes to handle the actions.
	CoveryDialog.prototype.getActionProcess = function(action) {
		if (action === 'upload') {
			this.getActions().forEach(null, function(actionWidget) {
				actionWidget.setDisabled(true);
			});
			this.content.toggle(false); // hide
			this.progressStatusContent.toggle(true); // show
			this.setProgressStatus('Uploading...', 1);
			var self = this;

			var fileTitle = mw.Title.newFromFileName(this.titleInput.getValue());
			return new OO.ui.Process(function() {
				var platformValues = this.platformInput.getItems().map(function(item) {
					return item.getData();
				});
				return (
					this.uploaded ||
					uploadFile(
						this.api,
						this.resizedFile,
						makeDescriptionText(
							new mw.Title(this.pageName),
							this.developerInput.getValue(),
							this.publisherInput.getValue(),
							platformValues
						),
						fileTitle
					).then(
						function() {
							return true;
						},
						function(errorCode) {
							self.setProgressError.call(self, 'Failed', 1);
							return $.Deferred().reject(
								new OO.ui.Error('Error uploading: ' + errorCode)
							);
						}
					)
				);
			}, this)
				.next(function() {
					this.uploaded = true;
					this.setProgressStatus('Uploaded file!', 25);
				}, this)
				.next(function() {
					this.setProgressStatus('Uploaded file! Creating file talk page...', 26);
					return (
						this.createdFileTalkpage ||
						createFileTalkpage(this.api, fileTitle).then(
							function() {
								return true;
							},
							function(errorCode) {
								self.setProgressError.call(
									self,
									'Uploaded file! Failed to create file talk page.',
									26
								);
								return $.Deferred().reject(
									new OO.ui.Error('Error creating file talk page: ' + errorCode)
								);
							}
						)
					);
				}, this)
				.next(function() {
					this.createdFileTalkpage = true;
					this.setProgressStatus('Uploaded file! Created file talk page!', 50);
				}, this)
				.next(function() {
					this.setProgressStatus(
						'Uploaded file! Created file talk page! Updating article...',
						51
					);
					var updatedParams = [
						{ name: 'image', value: fileTitle.getMainText() },
						{ name: 'caption', value: this.captionInput.getValue() },
						{ name: 'alt', value: this.altTextInput.getValue() },
						{ name: 'publisher', value: this.publisherInput.getValue() },
						{ name: 'developer', value: this.developerInput.getValue() }
					];
					return (
						this.updatedArticle ||
						updatePage(this.api, this.pageName, this.infobox, updatedParams).then(
							function() {
								return true;
							},
							function(errorCode) {
								self.setProgressError.call(
									self,
									'Uploaded file! Created file talk page! Failed to update article.',
									51
								);
								return $.Deferred().reject(
									new OO.ui.Error('Error editing article: ' + errorCode)
								);
							}
						)
					);
				}, this)
				.next(function() {
					this.updatedArticle = true;
					this.setProgressStatus(
						'Uploaded file! Created file talk page! Updated article!',
						75
					);
				}, this)
				.next(function() {
					this.setProgressStatus(
						'Uploaded file! Created file talk page! Updated article! Updating article talk page...',
						76
					);
					return updateTalkpage(this.api, this.pageName).then(
						function() {
							return true;
						},
						function(errorCode) {
							self.setProgressError.call(
								self,
								'Uploaded file! Created file talk page! Updated article! Failed to update article talk page.',
								76
							);
							return $.Deferred().reject(
								new OO.ui.Error('Error editing article talk page: ' + errorCode)
							);
						}
					);
				}, this)
				.next(function() {
					this.setProgressStatus('All done! Reloading article...', 100);
					return 1200;
				}, this)
				.next(function() {
					return this.close({ sucess: true });
				}, this);
		} else if (action === 'cancel') {
			return new OO.ui.Process(function() {
				return this.close();
			}, this);
		}
		// Fallback to parent handler
		return CoveryDialog.super.prototype.getActionProcess.call(this, action);
	};

	// Use the getTeardownProcess() method to perform actions whenever the dialog is closed.
	// This method provides access to data passed into the window's close() method
	// or the window manager's closeWindow() method.
	CoveryDialog.prototype.getTeardownProcess = function(data) {
		return CoveryDialog.super.prototype.getTeardownProcess.call(this, data).first(function() {
			// Perform any cleanup as needed
		}, this);
	};

	var showDialog = function showDialog(data) {
		var coveryWindowFactory = new OO.Factory();
		coveryWindowFactory.register(CoveryDialog);
		var mainWindowManager = new OO.ui.WindowManager({
			factory: coveryWindowFactory
		});
		$('body').append(mainWindowManager.$element);
		var instance = mainWindowManager.openWindow('coveryDialog', data);
		return instance.closed;
	};

	var startCovery = function startCovery(api, pageName) {
		return getLeadWikitext(api, pageName)
			.then(getInfoboxTemplate)
			.then(checkInfobox)
			.then(function(infobox) {
				return showDialog({
					api: api,
					pageName: pageName,
					infobox: infobox
				});
			})
			.then(
				function(data) {
					if (data && data.sucess) {
						window.location.reload();
					}
				},
				function(error) {
					var errorIsString = error === error.toString();
					var errorMessage = errorIsString ? 'Error: ' + error : error.toString();
					OO.ui.alert(errorMessage);
				}
			);
	};

	$.when(isSuitable(), $.ready()).then(function(config) {
		var portletLink = mw.util.addPortletLink('p-tb', '#', 'Upload cover', 'tb-covery');
		$(portletLink).click(function(e) {
			e.preventDefault();
			var api = new mw.Api({
				ajax: {
					headers: {
						'Api-User-Agent':
							SCRIPT.name +
							'/' +
							SCRIPT.version +
							' ( https://en.wikipedia.org/wiki/User:Evad37/Covery )'
					}
				}
			});
			startCovery(api, config.wgPageName);
		});
	});
}; // end of main wrapper function

mw.loader
	.using([
		'mediawiki.util',
		'mediawiki.api',
		'oojs-ui-core',
		'oojs-ui-widgets',
		'oojs-ui-windows'
	])
	.then(setupCovery);

// </nowiki>