MediaWiki:Gadget-fastcci.js

Note: After saving, you have to bypass your browser's cache to see the changes. Internet Explorer: press Ctrl-F5, Mozilla: hold down Shift while clicking Reload (or press Ctrl-Shift-R), Opera/Konqueror: press F5, Safari: hold down Shift + Alt while clicking Reload, Chrome: hold down Shift while clicking Reload.
/**
 * Add buttons to show all Featured pictures, Featured videos, Quality images, or Valued images
 * in and below the current category.
 * @author [[User:Dschwen]], 2014
 */

/* global mw, $ */

$( function () {
	var $slider, $link, $controls, depthThreshold, request, modal,
		// database backend url
		serverList = [ '//fastcci1.wmflabs.org/', '//fastcci2.wmflabs.org/' ],
		url = serverList[ Math.floor( Math.random() * serverList.length ) ],
		base = '//upload.wikimedia.org/wikipedia/commons/thumb/',
		badge = [
			'e/e7/Cscr-featured.svg/24px-Cscr-featured.svg.png',
			'a/a5/FV_invert_logo.png/24px-FV_invert_logo.png',
			'8/8c/Quality_images_logo.svg/24px-Quality_images_logo.svg.png',
			'd/d7/Valued_image_seal.svg/24px-Valued_image_seal.svg.png'
		],
		helpUrl = '//commons.wikimedia.org/wiki/Special:MyLanguage/Help:FastCCI',
		helpIcon = '4/44/Help-browser.svg/24px-Help-browser.svg.png',
		$e = $( '<div>' ).addClass( 'fastcci-results' ).on( 'click', '.fastcci-close', function () {
			$e.empty();
			tagLine( '' );
		} ),
		maxDepth = 0,
		minDepth = Infinity,
		// current namespace and action
		ns = mw.config.get( 'wgNamespaceNumber' ),
		namespaceIds = mw.config.get( 'wgNamespaceIds' ),
		action = mw.config.get( 'wgAction' ),
		// pageid of the current page
		thisPageId = mw.config.get( 'wgArticleId' ),
		uiLang = mw.config.get( 'wgUserLanguage' ),
		$content = $( '#bodyContent, #mw-content-text' ).eq( 0 ),
		$tagline = null,
		i18nData = {
			'Strong match': {
				ar: 'تطابق قوی',
				bn: 'জোরালো মিল',
				ce: 'Къовламе цхьаьнадогӀуш',
				cs: 'Bližší shoda',
				de: 'Starke Übereinstimmung',
				fa: 'تطابق قوی',
				fr: 'Correspondance forte',
				hr: 'Strogo podudaranje',
				it: 'Corrispondenza forte',
				mk: 'Строго совпаѓање',
				ml: 'നന്നായി ചേർച്ചയുള്ളവ',
				pt: 'Forte correspondência',
				ru: 'Строгое соответствие',
				sv: 'Stark överensstämmelse',
				zh: '强匹配'
			},
			'Weak match': {
				ar: 'تطابق ضعيف',
				bn: 'দুর্বল মিল',
				ce: 'Ледара цхьаьнадогӀуш',
				cs: 'Vzdálenější shoda',
				de: 'Schwache Übereinstimmung',
				fa: 'تطابق ضعیف',
				fr: 'Correspondance faible',
				hr: 'Približno podudaranje',
				it: 'Corrispondenza debole',
				mk: 'Благо совпаѓање',
				ml: 'ചെറുതായി ചേർച്ചയുള്ളവ',
				pt: 'Fraca correspondência',
				ru: 'Слабое соответствие',
				sv: 'Svag överensstämmelse',
				zh: '弱匹配'
			},
			'No results.': {
				ar: 'لا نتائج',
				bn: 'কোন ফলাফল নেই',
				ce: 'Хиламаш бац',
				cs: 'Žádné výsledky.',
				de: 'Keine Ergebnisse.',
				es: 'Sin resultados.',
				fa: 'هیچ نتیجه\u200cای.',
				fr: 'Pas de résultats.',
				hr: 'Nema rezultata.',
				it: 'Nessun risultato.',
				mk: 'Нема резултати.',
				ml: 'ഫലങ്ങളൊന്നുമില്ല',
				pt: 'Sem resultados.',
				ru: 'Нет результатов.',
				sv: 'Inga resultat.',
				tr: 'Sonuç yok.',
				zh: '无结果。'
			},
			'Connecting...': {
				ar: 'اتصال...',
				bn: 'সংযোগ হচ্ছে...',
				ce: 'Тасаялар...',
				cs: 'Připojuji se...',
				de: 'Verbinde ...',
				es: 'Estableciendo conexión...',
				fa: 'اتصال...',
				fr: 'Connexion...',
				hr: 'Povezujem se...',
				it: 'Connessione...',
				mk: 'Се поврзувам...',
				ml: 'എടുക്കുന്നു...',
				pt: 'A conectar...',
				ru: 'Соединение...',
				sv: 'Ansluter...',
				tr: 'Bağlanıyor...',
				zh: '连接中...'
			},
			'Computing...': {
				ar: 'حساب...',
				bn: 'গণনা চলছে...',
				ce: 'Таллам...',
				cs: 'Pracuji...',
				de: 'Arbeite ...',
				es: 'Calculando...',
				fa: 'محاسبه...',
				fr: 'Calcul...',
				hr: 'Računam...',
				it: 'Calcolo...',
				mk: 'Пресметувам...',
				ml: 'കണക്കാക്കുന്നു...',
				pt: 'A calcular...',
				ru: 'Анализ...',
				sv: 'Beräknar...',
				tr: 'Hesaplanıyor...',
				zh: '计算中...'
			},
			'Digging through NUM files...': {
				ar: 'جارِ البحث عن طريق ملفات  NUM...',
				bn: 'NUM ফাইলের মাধ্যমে সন্ধান...',
				ce: 'Файлийн таллам, NUM терахьца ...',
				cs: 'Prohledávám NUM souborů...',
				de: 'Durchsuche NUM Dateien ...',
				fa: 'استخراج از بین NUM پرونده...',
				fr: 'Fouille dans NUM fichiers...',
				hr: 'Pretražujem datoteke (NUM)...',
				it: 'Ricerca nei file NUM...',
				mk: 'Пребарувам по NUM податотеки...',
				ml: 'NUM പ്രമാണങ്ങൾ പരിശോധിക്കുന്നു...',
				pt: 'Procurando por ficheiros NUM...',
				ru: 'Анализ файлов, числом NUM ...',
				sv: 'Gräver genom NUM filer',
				zh: '已查找了NUM个文件...'
			},
			'Waiting in line. NUM ahead of us.': {
				ar: 'انتظار في الطابور. NUM تعمل حالياً.',
				bn: 'লাইনে অপেক্ষারত। আমাদের NUM এগিয়ে',
				ce: 'РогӀехь хьежар. Тхуна хьалхахь — NUM.',
				cs: 'Čekáme na řadu, jsme NUM.',
				de: 'Bitte warten, in der Warteschlange vor dir sind noch NUM Anfragen.',
				fa: 'ایستادن در خط. NUM جلوی ما.',
				fr: 'En attente. NUM requêtes devant nous.',
				hr: 'Pričekajte u redu. Broj zahtjeva na čekanju: NUM.',
				it: 'Attendi il tuo turno. Richieste NUM prima di noi.',
				mk: 'Чекам во редица. Пред нас има NUM.',
				ml: 'കാത്തിരിക്കുന്നു. NUM കൂടുതലാണ്.',
				pt: 'Aguarde na fila. NUM à nossa frente.',
				ru: 'Ожидание в очереди. Перед нами — NUM.',
				sv: 'Väntar i kö. NUM framför oss.',
				zh: '队列中,前边有NUM个文件。'
			},
			'Advanced...': {
				ar: 'متقدم...',
				bn: 'উন্নত...',
				ce: 'Хьалхадолу...',
				cs: 'Pokročilé...',
				de: 'Erweitert ...',
				es: 'Avanzado...',
				fa: 'پیشرفته...',
				fr: 'Avancé...',
				hr: 'Napredno...',
				it: 'Avanzate...',
				ml: 'വിപുലം...',
				pt: 'Avançado...',
				ru: 'Продвигаемся...',
				sv: 'Avancerat...',
				tr: 'İlerliyor...',
				zh: '高级...'
			},
			'Good pictures': {
				ar: 'صور جيدة',
				bn: 'ভালো চিত্রসমূহ',
				ce: 'Дика суьрташ',
				cs: 'Dobré obrázky',
				de: 'Gute Bilder',
				es: 'Imágenes buenas',
				fa: 'تصاویر خوب',
				fr: 'Bonnes images',
				hr: 'Dobre slike',
				it: 'Immagini belle',
				mk: 'Добри слики',
				ml: 'മികച്ച ചിത്രങ്ങൾ',
				pt: 'Imagens boas',
				ru: 'Хорошие изображения',
				sv: 'Bra bilder',
				tr: 'İyi resimler',
				zh: '优秀图片'
			},
			'All images': {
				ar: 'جميع الصور',
				bn: 'সব চিত্রসমূহ',
				ce: 'Массо суьрташ',
				cs: 'Všechny obrázky',
				de: 'Alle Bilder',
				es: 'Todas las imágenes',
				fa: 'همهٔ تصاویر',
				fr: 'Toutes les images',
				hr: 'Sve slike',
				it: 'Tutte le immagini',
				mk: 'Сите слики',
				ml: 'എല്ലാ ചിത്രങ്ങളും',
				pt: 'Todas as imagens',
				ru: 'Все изображения',
				sv: 'Alla bilder',
				tr: 'Tüm görseller',
				zh: '全部图像'
			},
			'Featured pictures': {
				ar: 'صور مختارة',
				bn: 'নির্বাচিত চিত্রসমূহ',
				ce: 'Хаьржина суьрташ',
				cs: 'Nejlepší obrázky',
				de: 'Exzellente Bilder',
				es: 'Imágenes destacadas',
				fa: 'تصاویر برگزیده',
				fr: 'Images remarquables',
				hr: 'Izabrane slike',
				it: 'Immagini in vetrina',
				mk: 'Избрани слики',
				ml: 'തിരഞ്ഞെടുത്ത ചിത്രങ്ങൾ',
				pt: 'Imagens em destaque',
				ru: 'Избранные изображения',
				sv: 'Utvalda bilder',
				tr: 'Seçkin resimler',
				zh: '特色图片'
			},
			'Featured videos': {
				ar: 'مقاطع الفيديو المميزة',
				bn: 'বৈশিষ্ট্যমূলক ভিডিওগুলি',
				ce: 'истакнути видео снимци',
				cs: 'Nejlepší videa',
				de: 'Exzellente Videos',
				es: 'Vídeos destacados',
				fa: 'ویدئوهای برگزیده',
				fr: 'Vidéo remarquables',
				hi: 'विशेष रुप से प्रदर्शित वीडियो',
				hr: 'istaknuti videozapisi',
				it: 'Video in vetrina',
				ja: '最高級の動画',
				mk: 'најдобрите видеа',
				ml: 'തിരഞ്ഞെടുത്ത വീഡിയോകൾ',
				nds: 'Grootoordige Videos',
				pl: 'Promowane filmy',
				pt: 'Vídeos em Destaque',
				ru: 'Избранные видео',
				sv: 'Utvalda videoklipp',
				tr: 'Seçkin videolar',
				vi: 'video hay nhất',
				zh: '精選視頻'
			},
			'Quality images': {
				ar: 'صور الجودة',
				bn: 'মানসম্মত চিত্রসমূহ',
				ce: 'ЦӀена суьрташ',
				cs: 'Kvalitní obrázky',
				de: 'Qualitätsbilder',
				es: 'Imágenes de calidad',
				fa: 'تصاویر باکیفیت',
				fr: 'Images de qualité',
				hr: 'Kvalitetne slike',
				it: 'Immagini di qualità',
				mk: 'Квалитетни слики',
				ml: 'മേന്മയേറിയ ചിത്രങ്ങൾ',
				pt: 'Imagens de qualidade',
				ru: 'Качественные изображения',
				sv: 'Kvalitetsbilder',
				tr: 'Kaliteli görseller',
				zh: '优质图像'
			},
			'Valued images': {
				ar: 'صور قيمة',
				bn: 'মূল্যবান চিত্রসমূহ',
				ce: 'Мехала суьрташ',
				cs: 'Hodnotné obrázky',
				de: 'Wertvolle Bilder',
				fa: 'تصاویر ارزشمند',
				fr: 'Images de valeur',
				hr: 'Cijenjene slike',
				it: 'Immagini di valore',
				mk: 'Ценети слики',
				ml: 'മൂല്യമേറിയ ചിത്രങ്ങൾ',
				pt: 'Imagens de valor',
				ru: 'Ценные иллюстрации',
				sv: 'Värdefulla bilder',
				tr: 'Değerli görseller',
				zh: '最有价值图像'
			},
			'Find images': {
				ar: 'البحث عن صور',
				bn: 'চিত্রসমূহ খুঁজুন',
				ce: 'Лаха сурт',
				cs: 'Najít obrázky',
				de: 'Finde Bilder',
				es: 'Encontrar imágenes',
				fa: 'یافتن تصاویر',
				fr: 'Trouver les images',
				hr: 'Pronađi slike',
				it: 'Trova le immagini',
				mk: 'Пронајди слики',
				ml: 'ചിത്രങ്ങൾ എടുക്കുക:',
				pt: 'Procurar imagens',
				ru: 'Поиск изображений',
				sv: 'Hitta bilder',
				tr: 'Görsel bul',
				zh: '搜索图像'
			},
			'in this category': {
				ar: 'في هذا التصنيف',
				bn: 'এই বিষয়শ্রেণীতে',
				ce: 'хӀокху категореш',
				cs: 'v této kategorii',
				de: 'aus dieser Kategorie',
				es: 'en esta categoría',
				fa: 'در این رده',
				fr: 'dans cette catégorie',
				hr: 'u ovoj kategoriji',
				it: 'in questa categoria',
				mk: 'во категоријава',
				ml: 'ഈ വർഗ്ഗത്തിലെ',
				pt: 'nesta categoria',
				ru: 'в этой категории',
				sv: 'i denna kategori',
				tr: 'bu kategoride',
				zh: '在本分类'
			},
			'and in': {
				ar: 'أضف إلى',
				bn: 'এবং এতে',
				ce: 'кхин чохь',
				cs: 'a zároveň v',
				de: 'die auch sind in',
				es: 'y en',
				mk: 'и во',
				fa: 'و در',
				fr: 'et dans',
				hr: 'kao i u',
				it: 'e in',
				ml: 'ഒപ്പം ഇതിലേയും',
				pt: 'e em',
				ru: 'и в',
				sv: 'och i',
				tr: 've burada',
				zh: '且在'
			},
			'but not in': {
				ar: 'لا تضع في',
				bn: 'কিন্তু এতে নয়',
				ce: 'амма чохь хӀума яц',
				cs: 'ale ne v',
				fa: 'ولی نه در',
				de: 'die nicht sind in',
				es: 'pero no en',
				fr: 'mais pas dans',
				hr: 'ali ne u',
				it: 'ma non in',
				mk: 'но не во',
				ml: 'ഇതിൽ ഉള്ളത് വേണ്ട',
				pt: 'mas não em',
				ru: 'но не в',
				sv: 'men inte i',
				tr: 'burada olmayan',
				zh: '但不在'
			},
			category: {
				ar: 'تصنيف',
				bn: 'বিষয়শ্রেণী',
				ce: 'категори',
				cs: 'kategorie',
				de: 'Kategorie',
				es: 'categoría',
				fa: 'رده',
				fr: 'catégorie',
				hr: 'kategorija',
				it: 'categoria',
				mk: 'категорија',
				ml: 'വർഗ്ഗം',
				pt: 'categoria',
				ru: 'категория',
				sv: 'kategori',
				tr: 'kategori',
				zh: '分类'
			},
			'In this category <b>and</b> in...': {
				ar: 'في هذا التصنيف <b>و</b> في...',
				bn: 'এই বিষয়শ্রেণীতে <b>এবং</b> এতে...',
				ce: 'ХӀокху категореш чохь <b>кхин</b> чохь...',
				cs: 'V této kategorii <b>a zároveň</b> v…',
				de: 'In dieser Kategorie <b>und</b> in ...',
				es: 'En esta categoría <b>y</b> en...',
				fa: 'در این رده <b>و</b> در...',
				fr: 'Dans cette catégorie <b>et</b> dans...',
				hr: 'U ovoj kategoriji <b>i</b> u...',
				it: 'In questa categoria <b>e</b> in...',
				mk: 'во категоријава <b>и</b> во...',
				ml: 'ഈ വർഗ്ഗത്തിലേയും <b>ഒപ്പം</b> ഇതിലേയും...',
				pt: 'Nesta categoria <b>e</b> em...',
				ru: 'В этой категории <b>и</b> в...',
				sv: 'I denna kategori <b>och</b> i...',
				tr: 'Bu kategoride <b>ve</b> şurada olanlar...',
				zh: '在本分类<b>且</b>在...'
			},
			'In this category <b>but not</b> in...': {
				ar: 'في هذا التصنيف <b>وليس</b> في...',
				bn: 'এই বিষয়শ্রেণীতে <b>কিন্তু</b> এতে নয়...',
				cs: 'V této kategorii, <b>ale ne</b> v…',
				de: 'In dieser Kategorie, <b>aber nicht</b> in ...',
				es: 'En esta categoría <b>pero no</b> en...',
				fa: 'در این رده <b>ولی نه</b> در...',
				fr: 'Dans cette catégorie <b>mais pas</b> dans...',
				hr: 'U ovoj kategoriji <b>ali ne</b> u...',
				it: 'In questa categoria <b>ma non</b> in...',
				mk: 'во категоријава <b>но не</b> во...',
				ml: 'ഈ വർഗ്ഗത്തിലേയും <b>പക്ഷേ</b> ഇതിലില്ലാത്തവയും...',
				pt: 'Nesta categoria <b>mas não</b> em...',
				ru: 'В этой категории, <b>но не</b> в...',
				sv: 'I denna kategori <b>men inte</b> i...',
				tr: 'Bu kategoride olan <b>ancak</b> şurada olmayanlar...',
				zh: '在本分类<b>但不</b>在...'
			},
			'More...': {
				ar: 'المزيد...',
				bn: 'আরও...',
				ce: 'Кхин...',
				cs: 'Další…',
				de: 'Weitere ...',
				es: 'Más...',
				fa: 'بیشتر...',
				fr: 'Plus...',
				hr: 'Više...',
				it: 'Altro...',
				mk: 'Повеќе...',
				ml: 'കൂടുതൽ...',
				pt: 'Mais...',
				ru: 'Ещё...',
				sv: 'Mer...',
				tr: 'Daha fazla...',
				zh: '更多...'
			},
			'About FastCCI...': {
				ar: 'حول FastCCI...',
				bn: 'FastCCI সম্পর্কে...',
				cs: 'O FastCCI…',
				ce: 'Цунах лаьцна FastCCI...',
				de: 'Über FastCCI ...',
				es: 'Acerca de FastCCI...',
				fa: 'دربارهٔ FastCCI...',
				hr: 'O FastCCIju',
				it: 'A proposito di FastCCI...',
				ml: 'FastCCI വിവരണം...',
				pt: 'Acerca de FastCCI...',
				ru: 'Описание FastCCI...',
				sv: 'Om FastCCI...',
				tr: 'FastCCI hakkında...',
				zh: '关于FastCCI...'
			},
			Ok: {
				ar: 'موافق',
				de: 'OK',
				es: 'Aceptar',
				hr: 'U redu',
				it: 'Va bene',
				pt: 'OK',
                ru: 'OK',
				sv: 'OK',
				tr: 'Tamam',
				zh: 'OK'
			},
			Cancel: {
				ar: 'إلغاء',
				de: 'Abbrechen',
				es: 'Cancelar',
				hr: 'Odustani',
				it: 'Annulla',
				pt: 'Cancelar',
                ru: 'Отмена',
				sv: 'Avbryt',
				tr: 'İptal',
				zh: '取消'
			}
		};

	// get a translated string
	function i18n( key ) {
		if ( !( key in i18nData ) || !( uiLang in i18nData[ key ] ) ) {
			return key;
		}
		return i18nData[ key ][ uiLang ];
	}

	// request the fastcci db over HTTPS (no streaming)
	function requestXHR( params, callback ) {
		$.get( 'https:' + url, params )
			.then( function ( data ) {
				var i, res = data.split( '\n' );
				for ( i = 0; i < res.length; ++i ) {
					callback( res[ i ] );
				}
			} );
	}

	// request the fastcci db using a JS callback (no streaming, no CORS)
	function requestJS( params, callback ) {
		window.fastcciCallback = function ( res ) {
			var i;
			for ( i = 0; i < res.length; ++i ) {
				callback( res[ i ] );
			}
		};
		$.getScript( 'https:' + url + '?t=js&' + $.param( params ) );
	}

	// request the fastcci db over a WebSocket (streaming with progressive status updates)
	function requestSocket( params, callback ) {
		var ws = new WebSocket( 'wss:' + url + '?' + $.param( params ) );
		// ws.onmessage = function(event) { setTimeout(function() {callback(event.data);}, 0); };
		ws.onmessage = function ( event ) {
			callback( event.data );
		};
		ws.onerror = function () {
			// We should fall back to JS if the WS connection throws an error
			// However current Chrome versions throw a non-fatal error (reserved bits)
			// I'll need to fix this first before I can reenable the fallback :-/
			// mw.notify('Still connecting...');
			// request = requestJS;
			// request(params, callback);
		};
	}

	// determine request method (requestSocket > requestXHR > requestJS)
	request = ( 'WebSocket' in window ) ? requestSocket : ( ( 'withCredentials' in new XMLHttpRequest() ) ? requestXHR : requestJS );

	// process result by API call (res is a line returned by the server)
	function processResult( res, ctx, callback, append ) {
		var r = res.split( '|' ), t, l = r.length, i, pageids,
			// get ID,depth, and tag lists
			ids = Array( l ), depths = Array( l ), tags = Array( l ),
			// return data
			ret = append || [];

		// build lists
		for ( i = 0; i < l; ++i ) {
			t = r[ i ].split( ',' );
			ids[ i ] = t[ 0 ];
			depths[ i ] = parseInt( t[ 1 ], 10 );
			tags[ i ] = parseInt( t[ 2 ] || '0', 10 );
		}

		// pageid list for query
		pageids = ids.join( '|' );

		// query all IDs
		$.get( mw.util.wikiScript( 'api' ), {
			action: 'query',
			pageids: pageids,
			format: 'json',
			utf8: true,
			prop: 'imageinfo|info',
			iiprop: 'size|user|sha1', inprop: 'url'
		} )
			.done( function ( data ) {
				var j,
					l = ids.length,
					p = data.query.pages;
				for ( j = 0; j < l; ++j ) {
					if ( ids[ j ] in p ) {
						p[ ids[ j ] ].fastcciDepth = depths[ j ];
						p[ ids[ j ] ].fastcciTag = tags[ j ];
						ret.push( p[ ids[ j ] ] );
					} else {
						ret.push( null );
					}
				}
				callback( ret, ctx );
			} );
	}

	// breadcrumbs (TODO: this breaks if the server returns two result lines. We need a reliable way to aggregate the results)
	function breadCrumbs( txt ) {
		var token = txt.split( ' ' );

		if ( token.length !== 2 || token[ 0 ] !== 'RESULT' ) {
			return;
		}
		processResult( token[ 1 ], null, function ( trail ) {
			var l = trail.length, i, bc = [];
			for ( i = 0; i < l; ++i ) {
				if ( 'fullurl' in trail[ i ] && 'title' in trail[ i ] ) {
					bc.push( '<a href="' + trail[ i ].fullurl + '">' + trail[ i ].title.replace( /^Category:/, '' ) + '</a>' );
				} else {
					bc.push( '???' );
				}
			}
			$content.prepend( $( '<div>' ).addClass( 'fastcci-breadcrumbs' ).html( bc.join( ' &rarr; ' ) ) );
		} );
	}

	// request wrapper that prepares the gallery
	function fetchGallery( params ) {
		var numResult = 0, dbAge = 0;
		maxDepth = 0;
		minDepth = Infinity;

		// strength-of-match slider is moved, change result set
		function slideMove( event, ui ) {
			var i;
			if ( ui.value === depthThreshold ) {
				return;
			}
			depthThreshold = ui.value;
			for ( i = minDepth; i <= maxDepth; ++i ) {
				if ( i > depthThreshold ) {
					$( '.fastcci-depth' + i ).hide();
				} else {
					$( '.fastcci-depth' + i ).show();
				}
			}
		}

		// append to result gallery
		function addToGallery( txt ) {
			var age,
				token = txt.split( ' ' ),
				d = 300;

			// no results yet
			if ( numResult === 0 ) {
				switch ( token[ 0 ] ) {
					case 'DONE':
						$e.text( i18n( 'No results.' ) );
						return;
					case 'QUEUED':
						$e.text( i18n( 'Waiting in line. NUM ahead of us.' ).replace( 'NUM', token[ 1 ] ) );
						return;
					case 'COMPUTE_START':
						$e.text( i18n( 'Computing...' ) );
						return;
					case 'WORKING':
						$e.text( i18n( 'Digging through NUM files...' ).replace( 'NUM', parseInt( token[ 1 ], 10 ) + parseInt( token[ 2 ], 10 ) ) );
						return;
				}
			}

			// are we done? add a ''More...'' button if applicable
			switch ( token[ 0 ] ) {
				case 'DONE':
					// human readable database age
					if ( dbAge < 60 ) {
						age = dbAge + 's';
					} else if ( dbAge < 3600 ) {
						age = Math.round( dbAge / 60 ) + 'm';
					} else if ( dbAge < 86400 ) {
						age = Math.round( dbAge / 3600 ) + 'h';
					} else {
						age = Math.round( dbAge / 86400 ) + 'd';
					}
					$e.append( $( '<div>' ).addClass( 'fastcci-resultstatus' ).text( age ) );

					// if we got the full amount of results show the button (TODO: look at OUTOF)
					if ( numResult === params.s ) {
						$e.append( $( '<button>' ).text( i18n( 'More...' ) ).button().on( 'click', function () {
							var s = params.s || 200,
								o = params.o || 0;
							params.o = o + s;
							window.scrollTo( 0, 0 );
							fetchGallery( params );
						} ) );
					}
					return;
				case 'DBAGE':
					dbAge = parseInt( token[ 1 ] || '0', 10 );
					return;
			}

			// beyond this point ony process RESULT responses
			if ( token.length < 2 || token[ 0 ] !== 'RESULT' ) {
				return;
			}

			// show controls if results are coming in
			if ( numResult === 0 ) {
				$e.empty().append( $controls );
				$link.attr( 'href', location.pathname + '?fastcci=' + encodeURIComponent( JSON.stringify( params ) ) );
				depthThreshold = 1000;
				$slider.slider( { change: slideMove, slide: slideMove, stop: slideMove, value: depthThreshold } );
			}

			// count the number of results received
			numResult += token[ 1 ].split( '|' ).length;

			processResult( token[ 1 ], $( '<span>' ).appendTo( $e ), function ( ids ) {
				var j, ow, oh, w, h, p, i, t, depth, $div, path,
					l = ids.length;
				for ( j = 0; j < l; ++j ) {
					p = ids[ j ];
					if ( p === null || !( 'imageinfo' in p ) ) {
						continue;
					}

					depth = p.fastcciDepth;
					if ( depth > maxDepth ) {
						maxDepth = depth;
					}
					if ( depth < minDepth ) {
						minDepth = depth;
					}

					i = p.imageinfo[ 0 ];
					ow = i.width;
					oh = i.height;
					if ( ow > oh ) {
						w = Math.round( ow * d / oh );
						h = d;
					} else {
						h = Math.round( oh * d / ow );
						w = d;
					}

					// thumb.php only forks if the size requested is smaller than the full image!
					t = encodeURIComponent( new mw.Title( p.title ).getMain() );
					if ( Math.ceil( w ) >= ow ) {
						w = ow;
						h = oh;
						path = '/wiki/Special:Redirect/file/' + t;
					} else {
						// console.log('//upload.wikimedia.org/wikipedia/commons/thumb/' + i.sha1.substr(0,1) + '/'+i.sha1.substr(0,2) + '/' + t + '/' + Math.ceil(w) + 'px-' + t);
						path = '/w/index.php?title=Special:Redirect/file/' + t + '&width=' + Math.ceil( w );
					}

					$div = $( '<div>' )
						.addClass( 'fastcci-image' )
						.addClass( 'fastcci-depth' + depth )
						.css( {
							width: d + 'px',
							height: d + 'px'
						} )
						.append(
							$( '<a>' )
								.attr( 'href', p.fullurl + '?fastcci_from=' + thisPageId + '&' + $.param( params ) )
								.append(
									$( '<img>' )
										.attr( 'src', path )
										.css( {
											position: 'absolute',
											left: Math.round( -( w - d ) / 2 ) + 'px',
											top: Math.round( -( h - d ) / 2 ) + 'px'
										} )
								)
						);

					// add badge to thumb
					if ( p.fastcciTag > 0 && p.fastcciTag <= 4 ) {
						$( '<img>' ).addClass( 'fastcci-badge' ).attr( 'src', base + badge[ p.fastcciTag - 1 ] ).appendTo( $div );
					}

					$e.append( $div );
				}

				// set slider limits
				$slider.slider( {
					min: minDepth,
					max: maxDepth,
					value: Math.max( maxDepth, depthThreshold )
				} );
			} );
		}

		$e.empty().prependTo( $content ).text( i18n( 'Connecting...' ) );
		request( params, addToGallery );
	}

	function tagLine( html ) {
		if ( $tagline === null ) {
			$tagline = $( '<span>' ).addClass( 'fastcci-tagline' ).appendTo( $( '#firstHeading>span' ).eq( 0 ) );
		}
		$tagline.html( html );
	}

	// show the modal dialog for advanced options
	function showDialog( operation ) {
		var text;

		// fetch the page text of a soft redirect page and resolve the pageid of the redirect
		function resolveRedirect( t ) {
			modal.isRedirect[ t ] = true;
			$.get( mw.util.wikiScript( 'index' ), { action: 'raw', title: 'Category:' + t }, undefined, 'text' )
				.done( function ( data ) {
					var m = /\{\{[Cc]ategory[_ ]redirect\|([Cc]ategory:|)([^}]+)\}\}/.exec( data );
					if ( m !== null ) {
						$.getJSON( mw.util.wikiScript( 'api' ), { action: 'query', format: 'json', titles: 'Category:' + m[ 2 ], indexpageids: true } )
							.done( function ( data ) {
								var i = parseInt( data.query.pageids[ 0 ], 10 );
								if ( i >= 0 ) {
									modal.isRedirect[ t ] = i;
									validate();
								}
							} );
					}
				} );
		}

		// build the tagline for this operation
		function getTagLine() {
			var v = modal.$input.val();
			switch ( modal.operation ) {
				case 'and': return i18n( 'and in' ) + ' <i>' + v + '</i>';
				case 'not': return i18n( 'but not in' ) + ' <i>' + v + '</i>';
			}
			return '';
		}

		// get pageId of currently typed category (or undefined)
		function pageId() {
			var v = modal.$input.val();

			// is this an unresiolved redirect?
			if ( v in modal.isRedirect ) {
				if ( modal.isRedirect[ v ] === true ) {
					return undefined;
				} else {
					return modal.isRedirect[ v ];
				}
			}

			// page id in cache?
			if ( v in modal.pageIds ) {
				return modal.pageIds[ v ];
			} else {
				return undefined;
			}
		}

		// validate current category
		function validate() {
			if ( pageId() !== undefined ) {
				modal.$ok.button( 'enable' );
			} else {
				modal.$ok.button( 'disable' );
			}
		}

		// send query and close dialog
		function performOperation() {
			var id = pageId();
			if ( id !== undefined ) {
				fetchGallery( { c1: thisPageId, c2: id, d1: 15, d2: 15, s: 200, a: modal.operation } );
				tagLine( getTagLine() );
			} else {
				mw.notify( 'Error.' );
			}
			modal.$div.dialog( 'close' );
		}

		// build dialog on demand
		if ( !modal ) {
			modal = { cache: {}, pageIds: {}, isRedirect: {}, operation: null };

			// dialog window
			modal.$div = $( '<div>' );

			// input widget
			modal.$input = $( '<input>' ).attr( 'placeholder', i18n( 'category' ) ).appendTo( modal.$div ).autocomplete( {
				minLength: 2,
				source: function ( request, response ) {
					var term = request.term.replace( / /g, '_' );
					if ( term in modal.cache ) {
						response( modal.cache[ term ] );
						return;
					}

					$.getJSON( mw.util.wikiScript( 'api' ), {
						action: 'query', format: 'json',
						generator: 'allpages', gapprefix: term, gapnamespace: 14,
						prop: 'templates', tltemplates: 'Template:Category_redirect'
					}, function ( data ) {
						var list = [], a, i, t;
						if ( ( 'query' in data ) && ( 'pages' in data.query ) ) {
							a = data.query.pages;
							for ( i in a ) {
								if ( Object.prototype.hasOwnProperty.call( a, i ) ) {
									t = a[ i ].title.replace( /^Category:/, '' );
									modal.pageIds[ t ] = i;
									// check for soft redirects
									if ( 'templates' in a[ i ] && !( t in modal.isRedirect ) ) {
										resolveRedirect( t );
									}
									// add title to suggestion list
									list.push( t );
								}
							}
							modal.cache[ term ] = list;
						}
						validate();
						response( list.sort() );
					} );
				},
				change: validate,
				select: function ( e, ui ) {
					// manually set the input to the selected value and validate
					modal.$input.val( ui.item.value );
					validate();
					e.preventDefault();
				}
			} )
				.on( 'keyup', validate )
				.on( 'keypress', function ( e ) {
					if ( e.keyCode === $.ui.keyCode.ENTER && pageId() ) {
						performOperation();
					}
				} );

			// build dialog
			modal.$div.dialog( {
				autoOpen: false,
				modal: true,
				buttons: [
					{ text: i18n( 'Ok' ), click: performOperation },
					{ text: i18n( 'Cancel' ), click: function () { modal.$div.dialog( 'close' ); } }
				]
			} );

			// Ok button
			modal.$ok = $( 'button', modal.$div.parent() ).eq( 0 );
			validate();
		}

		// customize text for the selected operation
		text = {
			and: [ 'In this category <b>and</b> in...', 'and in' ],
			not: [ 'In this category <b>but not</b> in...', 'but not in' ]
		};
		modal.$div.dialog( 'option', 'title', i18n( text[ operation ][ 0 ] ) );
		modal.operation = operation;

		// show the dialog
		modal.$div.dialog( 'open' );
	}

	// build category FP/QI/VI UI
	function addCatUI() {
		var $box = $( '#firstHeading' ),
			$buttonset,
			$menu = $( '<ul>' ).addClass( 'fastcci-menu' ),
			$advanced = $( '<button>' ).text( i18n( 'Advanced...' ) )
				.button( {
					text: false,
					icons: { primary: 'ui-icon-triangle-1-s' }
				} )
				.on( 'click', function ( e ) {
					$menu.toggle();
					e.stopPropagation();
				} ),
			width;

		$buttonset = $( '<div>' ).addClass( 'fastcci-buttonset' )
			.attr( 'lang', uiLang )
			.append(
				$( '<button>' )
					.attr( 'title', i18n( 'Featured pictures' ) + ', ' + i18n( 'Featured videos' ) + ', ' + i18n( 'Quality images' ) + ', ' + i18n( 'Valued images' ) )
					.append(
						$( '<img>' ).attr( { id: 'fastcci-fqv1', src: base + badge[ 3 ] } ),
						$( '<img>' ).attr( { id: 'fastcci-fqv2', src: base + badge[ 2 ] } ),
						$( '<img>' ).attr( { id: 'fastcci-fqv3', src: base + badge[ 1 ] } ),
						$( '<img>' ).attr( { id: 'fastcci-fqv4', src: base + badge[ 0 ] } ),
						$( '<span>' ).attr( 'id', 'fastcci-buttontextwrapper' ).append( $( '<span>' ).attr( 'id', 'fastcci-buttontext' ).text( i18n( 'Good pictures' ) ) )
					)
					.on( 'click', function () {
						fetchGallery( { c1: thisPageId, d1: 15, s: 200, a: 'fqv' } );
						tagLine( i18n( 'Good pictures' ) );
					} ),
				$advanced
			)
			.buttonset()
			.append( $menu )
			.appendTo( $box );

		width = $buttonset.outerWidth( true );

		$menu.append(
			$( '<li>' ).append( $( '<a>' ).attr( 'href', '#' )
				.append(
					$( '<span>' ).attr( 'id', 'fastcci-allimages' ).text( i18n( 'All images' ) )
				)
				.on( 'click', function () {
					fetchGallery( { c1: thisPageId, d1: 15, s: 200, a: 'list' } );
					tagLine( i18n( 'All images' ) );
				} )
			)
				.css( 'margin-bottom', '0.5em' ), // menu separator
			$( '<li>' ).append( $( '<a>' ).attr( 'href', '#' )
				.append(
					$( '<img>' ).addClass( 'fastcci-badges' ).attr( 'src', base + badge[ 0 ] ),
					i18n( 'Featured pictures' )
				)
				.on( 'click', function () {
					fetchGallery( { c1: thisPageId, c2: 3943817, d1: 15, d2: 0, s: 200 } );
					tagLine( i18n( 'Featured pictures' ) );
				} )
			),
			$( '<li>' ).append( $( '<a>' ).attr( 'href', '#' )
				.append(
					$( '<img>' ).addClass( 'fastcci-badges' ).attr( 'src', base + badge[ 1 ] ),
					i18n( 'Featured videos' )
				)
				.on( 'click', function () {
					fetchGallery( { c1: thisPageId, c2: 8460057, d1: 15, d2: 0, s: 200 } );
					tagLine( i18n( 'Featured videos' ) );
				} )
			),
			$( '<li>' ).append( $( '<a>' ).attr( 'href', '#' )
				.append(
					$( '<img>' ).addClass( 'fastcci-badges' ).attr( 'src', base + badge[ 2 ] ),
					i18n( 'Quality images' )
				)
				.on( 'click', function () {
					fetchGallery( { c1: thisPageId, c2: 3618826, d1: 15, d2: 0, s: 200 } );
					tagLine( i18n( 'Quality images' ) );
				} )
			),
			$( '<li>' ).append( $( '<a>' ).attr( 'href', '#' )
				.append(
					$( '<img>' ).addClass( 'fastcci-badges' ).attr( 'src', base + badge[ 3 ] ),
					i18n( 'Valued images' )
				)
				.on( 'click', function () {
					fetchGallery( { c1: thisPageId, c2: 4143367, d1: 15, d2: 0, s: 200 } );
					tagLine( i18n( 'Valued images' ) );
				} )
			)
				.css( 'margin-bottom', '0.5em' ), // menu separator
			$( '<li>' ).append( $( '<a>' ).attr( 'href', '#' )
				.append(
					$( '<img>' ).addClass( 'fastcci-badges' ).attr( 'src', base + '4/45/Fastcci_intersect.svg/24px-Fastcci_intersect.svg.png' ),
					i18n( 'In this category <b>and</b> in...' )
				)
				.on( 'click', function () {
					showDialog( 'and' );
					$menu.hide();
				} )
			),
			$( '<li>' ).append( $( '<a>' ).attr( 'href', '#' )
				.append(
					$( '<img>' ).addClass( 'fastcci-badges' ).attr( 'src', base + 'd/d5/Fastcci_notin.svg/24px-Fastcci_notin.svg.png' ),
					i18n( 'In this category <b>but not</b> in...' )
				)
				.on( 'click', function () {
					showDialog( 'not' );
					$menu.hide();
				} )
			)
				.css( 'margin-bottom', '0.5em' ), // menu separator
			$( '<li>' ).append( $( '<a>' ).attr( 'href', helpUrl ).attr( 'target', '_blank' )
				.append(
					$( '<img>' ).addClass( 'fastcci-badges' ).attr( 'src', base + helpIcon ),
					i18n( 'About FastCCI...' )
				)
				.on( 'click', function ( e ) {
					e.stopImmediatePropagation();
					$menu.hide();
				} )
			)
		)
			.menu()
			.position( {
				my: 'top',
				at: 'bottom',
				of: $advanced,
				within: $buttonset,
				collide: 'none'
			} )
			.hide();

		$slider = $( '<div>' ).addClass( 'fastcci-slider' );
		$link = $( '<a>' ).addClass( 'fastcci-help' )
			.append( $( '<img>' ).attr( 'src', '//upload.wikimedia.org/wikipedia/commons/f/fd/Link.png' ) );
		$controls = $( '<div>' ).addClass( 'fastcci-controls' )
			.append( i18n( 'Strong match' ) )
			.append( $slider )
			.append( i18n( 'Weak match' ) )
			.append(
				$( '<a>' ).addClass( 'fastcci-help' )
					.attr( 'href', helpUrl )
					.append( $( '<img>' ).attr( 'src', base + helpIcon ) )
			)
			.append( $link )
			.append( $( '<img>' )
				.attr( 'src', '//upload.wikimedia.org/wikipedia/commons/thumb/4/4c/Grey_close_x.svg/22px-Grey_close_x.svg.png' )
				.addClass( 'fastcci-close' )
			);

		// remove button text if space is insufficient
		function resize() {
			var space = $( '#firstHeading' ).width() - $( '#firstHeading>span' ).width() - 10; // 10 px safety
			if ( space < width ) {
				$( '#fastcci-buttontext' ).hide();
			} else {
				$( '#fastcci-buttontext' ).show();
			}
		}
		resize();
		$( window ).on( 'resize', resize );
		$( document ).on( 'click', function () {
			$menu.hide();
		} );
	}

	// add category UI
	if ( ns === namespaceIds.category && action === 'view' ) {
		mw.loader.using( [ 'jquery.ui'], addCatUI );

		// process url parameters (allows linking to results)
		var param, urlarg = mw.util.getParamValue( 'fastcci' );
		if ( urlarg ) {
			try {
				param = JSON.parse( urlarg );
				param.c1 = thisPageId; // make sure c1 is the current page to avoid surprises (like list pornstars on the kitten Category)
				param.s = param.s || 200;
				if ( param.s > 1000 ) {
					// limit number of results
					param.s = 1000;
				}
				mw.loader.using( 'jquery.ui', function () {
					fetchGallery( param );
				} );
			} catch ( e ) {
				mw.notify( 'FastCCI URL parameters invalid.' );
			}
		}
	}

	// display breadcrumbs on image page?
	var from = mw.util.getParamValue( 'fastcci_from' );
	if ( ns === namespaceIds.file && from ) {
		request( { c1: parseInt( from, 10 ), c2: thisPageId, a: 'path' }, breadCrumbs );
	}

	// display second breadcrumb for intersections
	var and_from = mw.util.getParamValue( 'c2' );
	if ( ns === namespaceIds.file && from ) {
		request( { c1: parseInt( and_from, 10 ), c2: thisPageId, a: 'path' }, breadCrumbs );
	}
} );