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