· 6 years ago · Oct 30, 2019, 04:06 AM
1/**
2 * Add buttons to show all Featured pictures, Featured videos, Quality images, or Valued images
3 * in and below the current category.
4 * @author [[User:Dschwen]], 2014
5 */
6
7/* global mw, $ */
8
9$( function () {
10 var $slider, $link, $controls, depthThreshold, request, modal,
11 // database backend url
12 serverList = [ '//fastcci1.wmflabs.org/', '//fastcci2.wmflabs.org/' ],
13 url = serverList[ Math.floor( Math.random() * serverList.length ) ],
14 base = '//upload.wikimedia.org/wikipedia/commons/thumb/',
15 badge = [ 'e/e7/Cscr-featured.svg/24px-Cscr-featured.svg.png',
16 '7/70/Featured_videos_logo_square.svg/24px-Featured_videos_logo_square.svg.png',
17 '8/8c/Quality_images_logo.svg/24px-Quality_images_logo.svg.png',
18 'd/d7/Valued_image_seal.svg/24px-Valued_image_seal.svg.png' ],
19 helpUrl = '//commons.wikimedia.org/wiki/Special:MyLanguage/Help:FastCCI',
20 helpIcon = '4/44/Help-browser.svg/24px-Help-browser.svg.png',
21 $e = $( '<div>' ).addClass( 'fastcci-results' ).on( 'click', '.fastcci-close', function () {
22 $e.empty();
23 tagLine( '' );
24 } ),
25 maxDepth = 0,
26 minDepth = Infinity,
27 // current namespace and action
28 ns = mw.config.get( 'wgNamespaceNumber' ),
29 namespaceIds = mw.config.get( 'wgNamespaceIds' ),
30 action = mw.config.get( 'wgAction' ),
31 // pageid of the current page
32 thisPageId = mw.config.get( 'wgArticleId' ),
33 uiLang = mw.config.get( 'wgUserLanguage' ),
34 $content = $( '#bodyContent, #mw-content-text' ).eq( 0 ),
35 $tagline = null,
36 i18nData = {
37 'Strong match': {
38 ar: 'تطابق قوی',
39 bn: 'জোরালো মিল',
40 ce: 'Къовламе цхьаьнадогӀуш',
41 cs: 'Bližší shoda',
42 de: 'Starke Übereinstimmung',
43 fa: 'تطابق قوی',
44 fr: 'Correspondance forte',
45 hr: 'Strogo podudaranje',
46 mk: 'Строго совпаѓање',
47 ml: 'നന്നായി ചേർച്ചയുള്ളവ',
48 pt: 'Forte correspondência',
49 ru: 'Строгое соответствие',
50 sv: 'Stark överensstämmelse'
51 },
52 'Weak match': {
53 ar: 'تطابق ضعيف',
54 bn: 'দুর্বল মিল',
55 ce: 'Ледара цхьаьнадогӀуш',
56 cs: 'Vzdálenější shoda',
57 de: 'Schwache Übereinstimmung',
58 fa: 'تطابق ضعیف',
59 fr: 'Correspondance faible',
60 hr: 'Približno podudaranje',
61 mk: 'Благо совпаѓање',
62 ml: 'ചെറുതായി ചേർച്ചയുള്ളവ',
63 pt: 'Fraca correspondência',
64 ru: 'Слабое соответствие',
65 sv: 'Svag överensstämmelse'
66 },
67 'No results.': {
68 ar: 'لا نتائج',
69 bn: 'কোন ফলাফল নেই',
70 ce: 'Хиламаш бац',
71 cs: 'Žádné výsledky.',
72 de: 'Keine Ergebnisse.',
73 fa: 'هیچ نتیجه\u200cای.',
74 fr: 'Pas de résultats.',
75 hr: 'Nema rezultata.',
76 mk: 'Нема резултати.',
77 ml: 'ഫലങ്ങളൊന്നുമില്ല',
78 pt: 'Sem resultados.',
79 ru: 'Нет результатов.',
80 sv: 'Inga resultat.'
81 },
82 'Connecting...': {
83 ar: 'اتصال...',
84 bn: 'সংযোগ হচ্ছে...',
85 ce: 'Тасаялар...',
86 cs: 'Připojuji se...',
87 de: 'Verbinde ...',
88 fa: 'اتصال...',
89 fr: 'Connexion...',
90 hr: 'Povezujem se...',
91 mk: 'Се поврзувам...',
92 ml: 'എടുക്കുന്നു...',
93 pt: 'A conectar...',
94 ru: 'Соединение...',
95 sv: 'Ansluter...'
96 },
97 'Computing...': {
98 ar: 'حساب...',
99 bn: 'গণনা চলছে...',
100 ce: 'Таллам...',
101 cs: 'Pracuji...',
102 de: 'Arbeite ...',
103 fa: 'محاسبه...',
104 fr: 'Calcul...',
105 hr: 'Računam...',
106 mk: 'Пресметувам...',
107 ml: 'കണക്കാക്കുന്നു...',
108 pt: 'A calcular...',
109 ru: 'Анализ...',
110 sv: 'Beräknar...'
111 },
112 'Digging through NUM files...': {
113 ar: 'جارِ البحث عن طريق ملفات NUM...',
114 bn: 'NUM ফাইলের মাধ্যমে সন্ধান...',
115 ce: 'Файлийн таллам, NUM терахьца ...',
116 cs: 'Prohledávám NUM souborů...',
117 de: 'Durchsuche NUM Dateien ...',
118 fa: 'استخراج از بین NUM پرونده...',
119 fr: 'Fouille dans NUM fichiers...',
120 hr: 'Pretražujem datoteke (NUM)...',
121 mk: 'Пребарувам по NUM податотеки...',
122 ml: 'NUM പ്രമാണങ്ങൾ പരിശോധിക്കുന്നു...',
123 pt: 'Procurando por ficheiros NUM...',
124 ru: 'Анализ файлов, числом NUM ...',
125 sv: 'Gräver genom NUM filer'
126 },
127 'Waiting in line. NUM ahead of us.': {
128 ar: 'انتظار في الطابور. NUM تعمل حالياً.',
129 bn: 'লাইনে অপেক্ষারত। আমাদের NUM এগিয়ে',
130 ce: 'РогӀехь хьежар. Тхуна хьалхахь — NUM.',
131 cs: 'Čekáme na řadu, jsme NUM.',
132 de: 'Warte darauf an die Reihe zu kommen. NUM Anfragen vor uns.',
133 fa: 'ایستادن در خط. NUM جلوی ما.',
134 fr: 'En attente. NUM requêtes devant nous.',
135 hr: 'Pričekajte u redu. Broj zahtjeva na čekanju: NUM.',
136 mk: 'Чекам во редица. Пред нас има NUM.',
137 ml: 'കാത്തിരിക്കുന്നു. NUM കൂടുതലാണ്.',
138 pt: 'Aguarde na fila. NUM à nossa frente.',
139 ru: 'Ожидание в очереди. Перед нами — NUM.',
140 sv: 'Väntar i kö. NUM framför oss.'
141 },
142 'Advanced...': {
143 ar: 'متقدم...',
144 bn: 'উন্নত...',
145 ce: 'Хьалхадолу...',
146 cs: 'Pokročilé...',
147 de: 'Erweitert ...',
148 fa: 'پیشرفته...',
149 fr: 'Avancé...',
150 hr: 'Napredno...',
151 ml: 'വിപുലം...',
152 pt: 'Avançado...',
153 ru: 'Продвигаемся...',
154 sv: 'Avancerat...'
155 },
156 'Good pictures': {
157 ar: 'صور جيدة',
158 bn: 'ভালো চিত্রসমূহ',
159 ce: 'Дика суьрташ',
160 cs: 'Dobré obrázky',
161 de: 'Gute Bilder',
162 es: 'Buenas imágenes',
163 fa: 'تصاویر خوب',
164 fr: 'Bonnes images',
165 hr: 'Dobre slike',
166 mk: 'Добри слики',
167 ml: 'മികച്ച ചിത്രങ്ങൾ',
168 pt: 'Imagens boas',
169 ru: 'Хорошие изображения',
170 sv: 'Bra bilder'
171 },
172 'All images': {
173 ar: 'جميع الصور',
174 bn: 'সব চিত্রসমূহ',
175 ce: 'Массо суьрташ',
176 cs: 'Všechny obrázky',
177 de: 'Alle Bilder',
178 fa: 'همهٔ تصاویر',
179 fr: 'Toutes les images',
180 hr: 'Sve slike',
181 mk: 'Сите слики',
182 ml: 'എല്ലാ ചിത്രങ്ങളും',
183 pt: 'Todas as imagens',
184 ru: 'Все изображения',
185 sv: 'Alla bilder'
186 },
187 'Featured pictures': {
188 ar: 'صور مختارة',
189 bn: 'নির্বাচিত চিত্রসমূহ',
190 ce: 'Хаьржина суьрташ',
191 cs: 'Nejlepší obrázky',
192 de: 'Exzellente Bilder',
193 fa: 'تصاویر برگزیده',
194 fr: 'Images remarquables',
195 hr: 'Izabrane slike',
196 mk: 'Избрани слики',
197 ml: 'തിരഞ്ഞെടുത്ത ചിത്രങ്ങൾ',
198 pt: 'Imagens em destaque',
199 ru: 'Избранные изображения',
200 sv: 'Utvalda bilder'
201 },
202 'Featured videos': {
203 ar: 'مقاطع فيديو جيدة',
204 bn: 'ভাল ভিডিও',
205 ce: 'Хаьржина суьрташ',
206 cs: 'Nejlepší obrázky',
207 de: 'Exzellente Video',
208 fa: 'تصاویر برگزیده',
209 fr: 'vidéo remarquables',
210 hr: 'Izabrane slike',
211 mk: 'Избрани слики',
212 ml: 'തിരഞ്ഞെടുത്ത ചിത്രങ്ങൾ',
213 pt: 'Imagens em destaque',
214 ru: 'хорошее видео',
215 sv: 'högkvalitativ video'
216 },
217 'Quality images': {
218 ar: 'صور الجودة',
219 bn: 'মানসম্মত চিত্রসমূহ',
220 ce: 'ЦӀена суьрташ',
221 cs: 'Kvalitní obrázky',
222 de: 'Qualitätsbilder',
223 fa: 'تصاویر باکیفیت',
224 fr: 'Images de qualité',
225 hr: 'Kvalitetne slike',
226 mk: 'Квалитетни слики',
227 ml: 'മേന്മയേറിയ ചിത്രങ്ങൾ',
228 pt: 'Imagens de qualidade',
229 ru: 'Качественные изображения',
230 sv: 'Kvalitetsbilder'
231 },
232 'Valued images': {
233 ar: 'صور قيمة',
234 bn: 'মূল্যবান চিত্রসমূহ',
235 ce: 'Мехала суьрташ',
236 cs: 'Hodnotné obrázky',
237 de: 'Wertvolle Bilder',
238 fa: 'تصاویر ارزشمند',
239 fr: 'Images de valeur',
240 hr: 'Cijenjene slike',
241 mk: 'Ценети слики',
242 ml: 'മൂല്യമേറിയ ചിത്രങ്ങൾ',
243 pt: 'Imagens de valor',
244 ru: 'Ценные иллюстрации',
245 sv: 'Värdefulla bilder'
246 },
247 'Find images': {
248 ar: 'البحث عن صور',
249 bn: 'চিত্রসমূহ খুঁজুন',
250 ce: 'Лаха сурт',
251 cs: 'Najít obrázky',
252 de: 'Finde Bilder',
253 fa: 'یافتن تصاویر',
254 fr: 'Trouver les images',
255 hr: 'Pronađi slike',
256 mk: 'Пронајди слики',
257 ml: 'ചിത്രങ്ങൾ എടുക്കുക:',
258 pt: 'Procurar imagens',
259 ru: 'Поиск изображений',
260 sv: 'Hitta bilder'
261 },
262 'in this category': {
263 ar: 'في هذا التصنيف',
264 bn: 'এই বিষয়শ্রেণীতে',
265 ce: 'хӀокху категореш',
266 cs: 'v této kategorii',
267 de: 'aus dieser Kategorie',
268 fa: 'در این رده',
269 fr: 'dans cette catégorie',
270 hr: 'u ovoj kategoriji',
271 mk: 'во категоријава',
272 ml: 'ഈ വർഗ്ഗത്തിലെ',
273 pt: 'nesta categoria',
274 ru: 'в этой категории',
275 sv: 'i denna kategori'
276 },
277 'and in': {
278 ar: 'أضف إلى',
279 bn: 'এবং এতে',
280 ce: 'кхин чохь',
281 cs: 'a zároveň v',
282 de: 'die auch sind in',
283 mk: 'и во',
284 fa: 'و در',
285 fr: 'et dans',
286 hr: 'kao i u',
287 ml: 'ഒപ്പം ഇതിലേയും',
288 pt: 'e em',
289 ru: 'и в',
290 sv: 'och i'
291 },
292 'but not in': {
293 ar: 'لا تضع في',
294 bn: 'কিন্তু এতে নয়',
295 ce: 'амма чохь хӀума яц',
296 cs: 'ale ne v',
297 fa: 'ولی نه در',
298 de: 'die nicht sind in',
299 fr: 'mais pas dans',
300 hr: 'ali ne u',
301 mk: 'но не во',
302 ml: 'ഇതിൽ ഉള്ളത് വേണ്ട',
303 pt: 'mas não em',
304 ru: 'но не в',
305 sv: 'men inte i'
306 },
307 category: {
308 ar: 'تصنيف',
309 bn: 'বিষয়শ্রেণী',
310 ce: 'категори',
311 cs: 'kategorie',
312 de: 'Kategorie',
313 fa: 'رده',
314 fr: 'catégorie',
315 hr: 'kategorija',
316 mk: 'категорија',
317 ml: 'വർഗ്ഗം',
318 pt: 'categoria',
319 ru: 'категория',
320 sv: 'kategori'
321 },
322 'In this category <b>and</b> in...': {
323 ar: 'في هذا التصنيف <b>و<b/> في...',
324 bn: 'এই বিষয়শ্রেণীতে <b>এবং</b> এতে...',
325 ce: 'ХӀокху категореш чохь <b>кхин</b> чохь...',
326 cs: 'V této kategorii <b>a zároveň</b> v…',
327 de: 'In dieser Kategorie <b>und</b> in ...',
328 fa: 'در این رده <b>و</b> در...',
329 fr: 'Dans cette catégorie <b>et</b> dans...',
330 hr: 'U ovoj kategoriji <b>i</b> u...',
331 mk: 'во категоријава <b>и</b> во...',
332 ml: 'ഈ വർഗ്ഗത്തിലേയും <b>ഒപ്പം</b> ഇതിലേയും...',
333 pt: 'Nesta categoria <b>e</b> em...',
334 ru: 'В этой категории <b>и</b> в...',
335 sv: 'I denna kategori <b>och</b> i...'
336 },
337 'In this category <b>but not</b> in...': {
338 ar: 'في هذا التصنيف <b>وليس<b/> في...',
339 bn: 'এই বিষয়শ্রেণীতে <b>কিন্তু</b> এতে নয়...',
340 cs: 'V této kategorii, <b>ale ne</b> v…',
341 de: 'In dieser Kategorie, <b>aber nicht</b> in ...',
342 fa: 'در این رده <b>ولی نه</b> در...',
343 fr: 'Dans cette catégorie <b>mais pas</b> dans...',
344 hr: 'U ovoj kategoriji <b>ali ne</b> u...',
345 mk: 'во категоријава <b>но не</b> во...',
346 ml: 'ഈ വർഗ്ഗത്തിലേയും <b>പക്ഷേ</b> ഇതിലില്ലാത്തവയും...',
347 pt: 'Nesta categoria <b>mas não</b> em...',
348 ru: 'В этой категории, <b>но не</b> в...',
349 sv: 'I denna kategori <b>men inte</b> i...'
350 },
351 'More...': {
352 ar: 'المزيد...',
353 bn: 'আরও...',
354 ce: 'Кхин...',
355 cs: 'Další…',
356 de: 'Weitere ...',
357 fa: 'بیشتر...',
358 fr: 'Plus...',
359 hr: 'Više...',
360 mk: 'Повеќе...',
361 ml: 'കൂടുതൽ...',
362 pt: 'Mais...',
363 ru: 'Ещё...',
364 sv: 'Mer...'
365 },
366 'About FastCCI...': {
367 ar: 'حول FastCCI...',
368 bn: 'FastCCI সম্পর্কে...',
369 cs: 'O FastCCI…',
370 ce: 'Цунах лаьцна FastCCI...',
371 de: 'Über FastCCI ...',
372 fa: 'دربارهٔ FastCCI...',
373 hr: 'O FastCCIju',
374 ml: 'FastCCI വിവരണം...',
375 pt: 'Acerca de FastCCI...',
376 ru: 'Описание FastCCI...',
377 sv: 'Om FastCCI...'
378 },
379 Ok: {
380 ar: 'موافق',
381 de: 'OK',
382 hr: 'U redu',
383 pt: 'OK',
384 sv: 'OK'
385 },
386 Cancel: {
387 ar: 'إلغاء',
388 de: 'Abbrechen',
389 hr: 'Odustani',
390 pt: 'Cancelar',
391 sv: 'Avbryt'
392 }
393 };
394
395 // get a translated string
396 function i18n( key ) {
397 if ( !( key in i18nData ) || !( uiLang in i18nData[ key ] ) ) {
398 return key;
399 }
400 return i18nData[ key ][ uiLang ];
401 }
402
403 // request the fastcci db over HTTPS (no streaming)
404 function requestXHR( params, callback ) {
405 $.get( 'https:' + url, params )
406 .then( function ( data ) {
407 var i, res = data.split( '\n' );
408 for ( i = 0; i < res.length; ++i ) {
409 callback( res[ i ] );
410 }
411 } );
412 }
413
414 // request the fastcci db using a JS callback (no streaming, no CORS)
415 function requestJS( params, callback ) {
416 window.fastcciCallback = function ( res ) {
417 var i;
418 for ( i = 0; i < res.length; ++i ) {
419 callback( res[ i ] );
420 }
421 };
422 $.getScript( 'https:' + url + '?t=js&' + $.param( params ) );
423 }
424
425 // request the fastcci db over a WebSocket (streaming with progressive status updates)
426 function requestSocket( params, callback ) {
427 var ws = new WebSocket( 'wss:' + url + '?' + $.param( params ) );
428 // ws.onmessage = function(event) { setTimeout(function() {callback(event.data);}, 0); };
429 ws.onmessage = function ( event ) {
430 callback( event.data );
431 };
432 ws.onerror = function () {
433 // We should fall back to JS if the WS connection throws an error
434 // However current Chrome versions throw a non-fatal error (reserved bits)
435 // I'll need to fix this first before I can reenable the fallback :-/
436 // mw.notify('Still connecting...');
437 // request = requestJS;
438 // request(params, callback);
439 };
440 }
441
442 // determine request method (requestSocket > requestXHR > requestJS)
443 request = ( 'WebSocket' in window && false ) ? requestSocket : ( ( 'withCredentials' in new XMLHttpRequest() ) ? requestXHR : requestJS );
444
445 // process result by API call (res is a line returned by the server)
446 function processResult( res, ctx, callback, append ) {
447 var r = res.split( '|' ), t, l = r.length, i, pageids,
448 // get ID,depth, and tag lists
449 ids = Array( l ), depths = Array( l ), tags = Array( l ),
450 // return data
451 ret = append || [];
452
453 // build lists
454 for ( i = 0; i < l; ++i ) {
455 t = r[ i ].split( ',' );
456 ids[ i ] = t[ 0 ];
457 depths[ i ] = parseInt( t[ 1 ], 10 );
458 tags[ i ] = parseInt( t[ 2 ] || '0', 10 );
459 }
460
461 // pageid list for query
462 pageids = ids.join( '|' );
463
464 // query all IDs
465 $.get( mw.util.wikiScript( 'api' ), {
466 action: 'query',
467 pageids: pageids,
468 format: 'json',
469 utf8: true,
470 prop: 'imageinfo|info',
471 iiprop: 'size|user|sha1', inprop: 'url'
472 } )
473 .done( function ( data ) {
474 var j,
475 l = ids.length,
476 p = data.query.pages;
477 for ( j = 0; j < l; ++j ) {
478 if ( ids[ j ] in p ) {
479 p[ ids[ j ] ].fastcciDepth = depths[ j ];
480 p[ ids[ j ] ].fastcciTag = tags[ j ];
481 ret.push( p[ ids[ j ] ] );
482 } else {
483 ret.push( null );
484 }
485 }
486 callback( ret, ctx );
487 } );
488 }
489
490 // breadcrumbs (TODO: this breaks if the server returns two result lines. We need a reliable way to aggregate the results)
491 function breadCrumbs( txt ) {
492 var token = txt.split( ' ' );
493
494 if ( token.length !== 2 || token[ 0 ] !== 'RESULT' ) {
495 return;
496 }
497 processResult( token[ 1 ], null, function ( trail ) {
498 var l = trail.length, i, bc = [];
499 for ( i = 0; i < l; ++i ) {
500 if ( 'fullurl' in trail[ i ] && 'title' in trail[ i ] ) {
501 bc.push( '<a href="' + trail[ i ].fullurl + '">' + trail[ i ].title.replace( /^Category:/, '' ) + '</a>' );
502 } else {
503 bc.push( '???' );
504 }
505 }
506 $content.prepend( $( '<div>' ).addClass( 'fastcci-breadcrumbs' ).html( bc.join( ' → ' ) ) );
507 } );
508 }
509
510 // request wrapper that prepares the gallery
511 function fetchGallery( params ) {
512 var numResult = 0, dbAge = 0;
513 maxDepth = 0;
514 minDepth = Infinity;
515
516 // strength-of-match slider is moved, change result set
517 function slideMove( event, ui ) {
518 var i;
519 if ( ui.value === depthThreshold ) {
520 return;
521 }
522 depthThreshold = ui.value;
523 for ( i = minDepth; i <= maxDepth; ++i ) {
524 if ( i > depthThreshold ) {
525 $( '.fastcci-depth' + i ).hide();
526 } else {
527 $( '.fastcci-depth' + i ).show();
528 }
529 }
530 }
531
532 // append to result gallery
533 function addToGallery( txt ) {
534 var age,
535 token = txt.split( ' ' ),
536 d = 300;
537
538 // no results yet
539 if ( numResult === 0 ) {
540 switch ( token[ 0 ] ) {
541 case 'DONE':
542 $e.text( i18n( 'No results.' ) );
543 return;
544 case 'QUEUED':
545 $e.text( i18n( 'Waiting in line. NUM ahead of us.' ).replace( 'NUM', token[ 1 ] ) );
546 return;
547 case 'COMPUTE_START':
548 $e.text( i18n( 'Computing...' ) );
549 return;
550 case 'WORKING':
551 $e.text( i18n( 'Digging through NUM files...' ).replace( 'NUM', parseInt( token[ 1 ], 10 ) + parseInt( token[ 2 ], 10 ) ) );
552 return;
553 }
554 }
555
556 // are we done? add a ''More...'' button if applicable
557 switch ( token[ 0 ] ) {
558 case 'DONE':
559 // human readable database age
560 if ( dbAge < 60 ) {
561 age = dbAge + 's';
562 } else if ( dbAge < 3600 ) {
563 age = Math.round( dbAge / 60 ) + 'm';
564 } else if ( dbAge < 86400 ) {
565 age = Math.round( dbAge / 3600 ) + 'h';
566 } else {
567 age = Math.round( dbAge / 86400 ) + 'd';
568 }
569 $e.append( $( '<div>' ).addClass( 'fastcci-resultstatus' ).text( age ) );
570
571 // if we got the full amount of results show the button (TODO: look at OUTOF)
572 if ( numResult === params.s ) {
573 $e.append( $( '<button>' ).text( i18n( 'More...' ) ).button().on( 'click', function () {
574 var s = params.s || 200,
575 o = params.o || 0;
576 params.o = o + s;
577 window.scrollTo( 0, 0 );
578 fetchGallery( params );
579 } ) );
580 }
581 return;
582 case 'DBAGE':
583 dbAge = parseInt( token[ 1 ] || '0', 10 );
584 return;
585 }
586
587 // beyond this point ony process RESULT responses
588 if ( token.length < 2 || token[ 0 ] !== 'RESULT' ) {
589 return;
590 }
591
592 // show controls if results are coming in
593 if ( numResult === 0 ) {
594 $e.empty().append( $controls );
595 $link.attr( 'href', location.pathname + '?fastcci=' + encodeURIComponent( JSON.stringify( params ) ) );
596 depthThreshold = 1000;
597 $slider.slider( { change: slideMove, slide: slideMove, stop: slideMove, value: depthThreshold } );
598 }
599
600 // count the number of results received
601 numResult += token[ 1 ].split( '|' ).length;
602
603 processResult( token[ 1 ], $( '<span>' ).appendTo( $e ), function ( ids ) {
604 var j, ow, oh, w, h, p, i, t, depth, $div, path,
605 l = ids.length;
606 for ( j = 0; j < l; ++j ) {
607 p = ids[ j ];
608 if ( p === null || !( 'imageinfo' in p ) ) {
609 continue;
610 }
611
612 depth = p.fastcciDepth;
613 if ( depth > maxDepth ) {
614 maxDepth = depth;
615 }
616 if ( depth < minDepth ) {
617 minDepth = depth;
618 }
619
620 i = p.imageinfo[ 0 ];
621 ow = i.width;
622 oh = i.height;
623 if ( ow > oh ) {
624 w = Math.round( ow * d / oh );
625 h = d;
626 } else {
627 h = Math.round( oh * d / ow );
628 w = d;
629 }
630
631 // thumb.php only forks if the size requested is smaller than the full image!
632 t = encodeURIComponent( new mw.Title( p.title ).getMain() );
633 if ( Math.ceil( w ) >= ow ) {
634 w = ow;
635 h = oh;
636 path = '/wiki/Special:Redirect/file/' + t;
637 } else {
638 // console.log('//upload.wikimedia.org/wikipedia/commons/thumb/' + i.sha1.substr(0,1) + '/'+i.sha1.substr(0,2) + '/' + t + '/' + Math.ceil(w) + 'px-' + t);
639 path = '/w/index.php?title=Special:Redirect/file/' + t + '&width=' + Math.ceil( w );
640 }
641
642 $div = $( '<div>' )
643 .addClass( 'fastcci-image' )
644 .addClass( 'fastcci-depth' + depth )
645 .css( {
646 width: d + 'px',
647 height: d + 'px'
648 } )
649 .append(
650 $( '<a>' )
651 .attr( 'href', p.fullurl + '?fastcci_from=' + thisPageId + '&' + $.param( params ) )
652 .append(
653 $( '<img>' )
654 .attr( 'src', path )
655 .css( {
656 position: 'absolute',
657 left: Math.round( -( w - d ) / 2 ) + 'px',
658 top: Math.round( -( h - d ) / 2 ) + 'px'
659 } )
660 )
661 );
662
663 // add badge to thumb
664 if ( p.fastcciTag > 0 && p.fastcciTag <= 3 ) {
665 $( '<img>' ).addClass( 'fastcci-badge' ).attr( 'src', base + badge[ p.fastcciTag - 1 ] ).appendTo( $div );
666 }
667
668 $e.append( $div );
669 }
670
671 // set slider limits
672 $slider.slider( {
673 min: minDepth,
674 max: maxDepth,
675 value: Math.max( maxDepth, depthThreshold )
676 } );
677 } );
678 }
679
680 $e.empty().prependTo( $content ).text( i18n( 'Connecting...' ) );
681 request( params, addToGallery );
682 }
683
684 function tagLine( html ) {
685 if ( $tagline === null ) {
686 $tagline = $( '<span>' ).addClass( 'fastcci-tagline' ).appendTo( $( '#firstHeading>span' ).eq( 0 ) );
687 }
688 $tagline.html( html );
689 }
690
691 // show the modal dialog for advanced options
692 function showDialog( operation ) {
693 var text;
694
695 // fetch the page text of a soft redirect page and resolve the pageid of the redirect
696 function resolveRedirect( t ) {
697 modal.isRedirect[ t ] = true;
698 $.get( mw.util.wikiScript( 'index' ), { action: 'raw', title: 'Category:' + t }, undefined, 'text' )
699 .done( function ( data ) {
700 var m = /\{\{[Cc]ategory[_ ]redirect\|([Cc]ategory:|)([^}]+)\}\}/.exec( data );
701 if ( m !== null ) {
702 $.getJSON( mw.util.wikiScript( 'api' ), { action: 'query', format: 'json', titles: 'Category:' + m[ 2 ], indexpageids: true } )
703 .done( function ( data ) {
704 var i = parseInt( data.query.pageids[ 0 ], 10 );
705 if ( i >= 0 ) {
706 modal.isRedirect[ t ] = i;
707 validate();
708 }
709 } );
710 }
711 } );
712 }
713
714 // build the tagline for this operation
715 function getTagLine() {
716 var v = modal.$input.val();
717 switch ( modal.operation ) {
718 case 'and': return i18n( 'and in' ) + ' <i>' + v + '</i>';
719 case 'not': return i18n( 'but not in' ) + ' <i>' + v + '</i>';
720 }
721 return '';
722 }
723
724 // get pageId of currently typed category (or undefined)
725 function pageId() {
726 var v = modal.$input.val();
727
728 // is this an unresiolved redirect?
729 if ( v in modal.isRedirect ) {
730 if ( modal.isRedirect[ v ] === true ) {
731 return undefined;
732 } else {
733 return modal.isRedirect[ v ];
734 }
735 }
736
737 // page id in cache?
738 if ( v in modal.pageIds ) {
739 return modal.pageIds[ v ];
740 } else {
741 return undefined;
742 }
743 }
744
745 // validate current category
746 function validate() {
747 if ( pageId() !== undefined ) {
748 modal.$ok.button( 'enable' );
749 } else {
750 modal.$ok.button( 'disable' );
751 }
752 }
753
754 // send query and close dialog
755 function performOperation() {
756 var id = pageId();
757 if ( id !== undefined ) {
758 fetchGallery( { c1: thisPageId, c2: id, d1: 15, d2: 15, s: 200, a: modal.operation } );
759 tagLine( getTagLine() );
760 } else {
761 mw.notify( 'Error.' );
762 }
763 modal.$div.dialog( 'close' );
764 }
765
766 // build dialog on demand
767 if ( !modal ) {
768 modal = { cache: {}, pageIds: {}, isRedirect: {}, operation: null };
769
770 // dialog window
771 modal.$div = $( '<div>' );
772
773 // input widget
774 modal.$input = $( '<input>' ).attr( 'placeholder', i18n( 'category' ) ).appendTo( modal.$div ).autocomplete( {
775 minLength: 2,
776 source: function ( request, response ) {
777 var term = request.term.replace( / /g, '_' );
778 if ( term in modal.cache ) {
779 response( modal.cache[ term ] );
780 return;
781 }
782
783 $.getJSON( mw.util.wikiScript( 'api' ), {
784 action: 'query', format: 'json',
785 generator: 'allpages', gapprefix: term, gapnamespace: 14,
786 prop: 'templates', tltemplates: 'Template:Category_redirect'
787 }, function ( data ) {
788 var list = [], a, i, t;
789 if ( ( 'query' in data ) && ( 'pages' in data.query ) ) {
790 a = data.query.pages;
791 for ( i in a ) {
792 if ( Object.prototype.hasOwnProperty.call( a, i ) ) {
793 t = a[ i ].title.replace( /^Category:/, '' );
794 modal.pageIds[ t ] = i;
795 // check for soft redirects
796 if ( 'templates' in a[ i ] && !( t in modal.isRedirect ) ) {
797 resolveRedirect( t );
798 }
799 // add title to suggestion list
800 list.push( t );
801 }
802 }
803 modal.cache[ term ] = list;
804 }
805 validate();
806 response( list.sort() );
807 } );
808 },
809 change: validate,
810 select: function ( e, ui ) {
811 // manually set the input to the selected value and validate
812 modal.$input.val( ui.item.value );
813 validate();
814 e.preventDefault();
815 }
816 } )
817 .on( 'keyup', validate )
818 .on( 'keypress', function ( e ) {
819 if ( e.keyCode === $.ui.keyCode.ENTER && pageId() ) {
820 performOperation();
821 }
822 } );
823
824 // build dialog
825 modal.$div.dialog( {
826 autoOpen: false,
827 modal: true,
828 buttons: [
829 { text: i18n( 'Ok' ), click: performOperation },
830 { text: i18n( 'Cancel' ), click: function () { modal.$div.dialog( 'close' ); } }
831 ]
832 } );
833
834 // Ok button
835 modal.$ok = $( 'button', modal.$div.parent() ).eq( 0 );
836 validate();
837 }
838
839 // customize text for the selected operation
840 text = {
841 and: [ 'In this category <b>and</b> in...', 'and in' ],
842 not: [ 'In this category <b>but not</b> in...', 'but not in' ]
843 };
844 modal.$div.dialog( 'option', 'title', i18n( text[ operation ][ 0 ] ) );
845 modal.operation = operation;
846
847 // show the dialog
848 modal.$div.dialog( 'open' );
849 }
850
851 // build category FP/QI/VI UI
852 function addCatUI() {
853 var $box = $( '#firstHeading' ),
854 $buttonset,
855 $menu = $( '<ul>' ).addClass( 'fastcci-menu' ),
856 $advanced = $( '<button>' ).text( i18n( 'Advanced...' ) )
857 .button( {
858 text: false,
859 icons: { primary: 'ui-icon-triangle-1-s' }
860 } )
861 .on( 'click', function ( e ) {
862 $menu.toggle();
863 e.stopPropagation();
864 } ),
865 width;
866
867 $buttonset = $( '<div>' ).addClass( 'fastcci-buttonset' )
868 .attr( 'lang', uiLang )
869 .append(
870 $( '<button>' )
871 .attr( 'title', i18n( 'Featured pictures' ) + ', ' + i18n( 'Featured videos' ) + ', ' + i18n( 'Quality images' ) + ', ' + i18n( 'Valued images' ) )
872 .append(
873 $( '<img>' ).attr( { id: 'fastcci-fqv1', src: base + badge[ 2 ] } ),
874 $( '<img>' ).attr( { id: 'fastcci-fqv2', src: base + badge[ 1 ] } ),
875 $( '<img>' ).attr( { id: 'fastcci-fqv3', src: base + badge[ 0 ] } ),
876 $( '<span>' ).attr( 'id', 'fastcci-buttontextwrapper' ).append( $( '<span>' ).attr( 'id', 'fastcci-buttontext' ).text( i18n( 'Good pictures' ) ) )
877 )
878 .on( 'click', function () {
879 fetchGallery( { c1: thisPageId, d1: 15, s: 200, a: 'fqv' } );
880 tagLine( i18n( 'Good pictures' ) );
881 } ),
882 $advanced
883 )
884 .buttonset()
885 .append( $menu )
886 .appendTo( $box );
887
888 width = $buttonset.outerWidth( true );
889
890 $menu.append(
891 $( '<li>' ).append( $( '<a>' ).attr( 'href', '#' )
892 .append(
893 $( '<span>' ).attr( 'id', 'fastcci-allimages' ).text( i18n( 'All images' ) )
894 )
895 .on( 'click', function () {
896 fetchGallery( { c1: thisPageId, d1: 15, s: 200, a: 'list' } );
897 tagLine( i18n( 'All images' ) );
898 } )
899 )
900 .css( 'margin-bottom', '0.5em' ), // menu separator
901 $( '<li>' ).append( $( '<a>' ).attr( 'href', '#' )
902 .append(
903 $( '<img>' ).addClass( 'fastcci-badges' ).attr( 'src', base + badge[ 0 ] ),
904 i18n( 'Featured pictures' )
905 )
906 .on( 'click', function () {
907 fetchGallery( { c1: thisPageId, c2: 3943817, d1: 15, d2: 0, s: 200 } );
908 tagLine( i18n( 'Featured pictures' ) );
909 } )
910 ),
911 $( '<li>' ).append( $( '<a>' ).attr( 'href', '#' )
912 .append(
913 $( '<img>' ).addClass( 'fastcci-badges' ).attr( 'src', base + badge[ 0 ] ),
914 i18n( 'Featured videos' )
915 )
916 .on( 'click', function () {
917 fetchGallery( { c1: thisPageId, c2: 8460057, d1: 15, d2: 0, s: 200 } );
918 tagLine( i18n( 'Featured videos' ) );
919 } )
920 ),
921 $( '<li>' ).append( $( '<a>' ).attr( 'href', '#' )
922 .append(
923 $( '<img>' ).addClass( 'fastcci-badges' ).attr( 'src', base + badge[ 1 ] ),
924 i18n( 'Quality images' )
925 )
926 .on( 'click', function () {
927 fetchGallery( { c1: thisPageId, c2: 3618826, d1: 15, d2: 0, s: 200 } );
928 tagLine( i18n( 'Quality images' ) );
929 } )
930 ),
931 $( '<li>' ).append( $( '<a>' ).attr( 'href', '#' )
932 .append(
933 $( '<img>' ).addClass( 'fastcci-badges' ).attr( 'src', base + badge[ 2 ] ),
934 i18n( 'Valued images' )
935 )
936 .on( 'click', function () {
937 fetchGallery( { c1: thisPageId, c2: 4143367, d1: 15, d2: 0, s: 200 } );
938 tagLine( i18n( 'Valued images' ) );
939 } )
940 )
941 .css( 'margin-bottom', '0.5em' ), // menu separator
942 $( '<li>' ).append( $( '<a>' ).attr( 'href', '#' )
943 .append(
944 $( '<img>' ).addClass( 'fastcci-badges' ).attr( 'src', base + '4/45/Fastcci_intersect.svg/24px-Fastcci_intersect.svg.png' ),
945 i18n( 'In this category <b>and</b> in...' )
946 )
947 .on( 'click', function () {
948 showDialog( 'and' );
949 $menu.hide();
950 } )
951 ),
952 $( '<li>' ).append( $( '<a>' ).attr( 'href', '#' )
953 .append(
954 $( '<img>' ).addClass( 'fastcci-badges' ).attr( 'src', base + 'd/d5/Fastcci_notin.svg/24px-Fastcci_notin.svg.png' ),
955 i18n( 'In this category <b>but not</b> in...' )
956 )
957 .on( 'click', function () {
958 showDialog( 'not' );
959 $menu.hide();
960 } )
961 )
962 .css( 'margin-bottom', '0.5em' ), // menu separator
963 $( '<li>' ).append( $( '<a>' ).attr( 'href', helpUrl ).attr( 'target', '_blank' )
964 .append(
965 $( '<img>' ).addClass( 'fastcci-badges' ).attr( 'src', base + helpIcon ),
966 i18n( 'About FastCCI...' )
967 )
968 .on( 'click', function ( e ) {
969 e.stopImmediatePropagation();
970 $menu.hide();
971 } )
972 )
973 )
974 .menu()
975 .position( {
976 my: 'top',
977 at: 'bottom',
978 of: $advanced,
979 within: $buttonset,
980 collide: 'none'
981 } )
982 .hide();
983
984 $slider = $( '<div>' ).addClass( 'fastcci-slider' );
985 $link = $( '<a>' ).addClass( 'fastcci-help' )
986 .append( $( '<img>' ).attr( 'src', '//upload.wikimedia.org/wikipedia/commons/f/fd/Link.png' ) );
987 $controls = $( '<div>' ).addClass( 'fastcci-controls' )
988 .append( i18n( 'Strong match' ) )
989 .append( $slider )
990 .append( i18n( 'Weak match' ) )
991 .append(
992 $( '<a>' ).addClass( 'fastcci-help' )
993 .attr( 'href', helpUrl )
994 .append( $( '<img>' ).attr( 'src', base + helpIcon ) )
995 )
996 .append( $link )
997 .append( $( '<img>' )
998 .attr( 'src', '//upload.wikimedia.org/wikipedia/commons/thumb/4/4c/Grey_close_x.svg/22px-Grey_close_x.svg.png' )
999 .addClass( 'fastcci-close' )
1000 );
1001
1002 // remove button text if space is insufficient
1003 function resize() {
1004 var space = $( '#firstHeading' ).width() - $( '#firstHeading>span' ).width() - 10; // 10 px safety
1005 if ( space < width ) {
1006 $( '#fastcci-buttontext' ).hide();
1007 } else {
1008 $( '#fastcci-buttontext' ).show();
1009 }
1010 }
1011 resize();
1012 $( window ).on( 'resize', resize );
1013 $( document ).on( 'click', function () {
1014 $menu.hide();
1015 } );
1016 }
1017
1018 // add category UI
1019 if ( ns === namespaceIds.category && action === 'view' ) {
1020 mw.loader.using( [ 'jquery.ui'], addCatUI );
1021
1022 // process url parameters (allows linking to results)
1023 var param, urlarg = mw.util.getParamValue( 'fastcci' );
1024 if ( urlarg ) {
1025 try {
1026 param = JSON.parse( urlarg );
1027 param.c1 = thisPageId; // make sure c1 is the current page to avoid surprises (like list pornstars on the kitten Category)
1028 param.s = param.s || 200;
1029 if ( param.s > 1000 ) {
1030 // limit number of results
1031 param.s = 1000;
1032 }
1033 mw.loader.using( 'jquery.ui', function () {
1034 fetchGallery( param );
1035 } );
1036 } catch ( e ) {
1037 mw.notify( 'FastCCI URL parameters invalid.' );
1038 }
1039 }
1040 }
1041
1042 // display breadcrumbs on image page?
1043 var from = mw.util.getParamValue( 'fastcci_from' );
1044 if ( ns === namespaceIds.file && from ) {
1045 request( { c1: parseInt( from, 10 ), c2: thisPageId, a: 'path' }, breadCrumbs );
1046 }
1047
1048 // display second breadcrumb for intersections
1049 var and_from = mw.util.getParamValue( 'c2' );
1050 if ( ns === namespaceIds.file && from ) {
1051 request( { c1: parseInt( and_from, 10 ), c2: thisPageId, a: 'path' }, breadCrumbs );
1052 }
1053} );