summaryrefslogtreecommitdiff
path: root/www/wiki/extensions/MultimediaViewer
diff options
context:
space:
mode:
authorYaco <franco@reevo.org>2020-06-04 11:01:00 -0300
committerYaco <franco@reevo.org>2020-06-04 11:01:00 -0300
commitfc7369835258467bf97eb64f184b93691f9a9fd5 (patch)
treedaabd60089d2dd76d9f5fb416b005fbe159c799d /www/wiki/extensions/MultimediaViewer
first commit
Diffstat (limited to 'www/wiki/extensions/MultimediaViewer')
-rw-r--r--www/wiki/extensions/MultimediaViewer/.eslintrc.json15
-rw-r--r--www/wiki/extensions/MultimediaViewer/.gitignore5
-rw-r--r--www/wiki/extensions/MultimediaViewer/.gitreview6
-rw-r--r--www/wiki/extensions/MultimediaViewer/.phpcs.xml9
-rw-r--r--www/wiki/extensions/MultimediaViewer/.rubocop.yml24
-rw-r--r--www/wiki/extensions/MultimediaViewer/.rubocop_todo.yml40
-rw-r--r--www/wiki/extensions/MultimediaViewer/.stylelintrc.json6
-rw-r--r--www/wiki/extensions/MultimediaViewer/AUTHORS24
-rw-r--r--www/wiki/extensions/MultimediaViewer/CODE_OF_CONDUCT.md1
-rw-r--r--www/wiki/extensions/MultimediaViewer/COPYING339
-rw-r--r--www/wiki/extensions/MultimediaViewer/Gemfile5
-rw-r--r--www/wiki/extensions/MultimediaViewer/Gemfile.lock113
-rw-r--r--www/wiki/extensions/MultimediaViewer/Gruntfile.js72
-rw-r--r--www/wiki/extensions/MultimediaViewer/MultimediaViewer.php132
-rw-r--r--www/wiki/extensions/MultimediaViewer/Rakefile17
-rw-r--r--www/wiki/extensions/MultimediaViewer/composer.json20
-rw-r--r--www/wiki/extensions/MultimediaViewer/extension.json444
-rw-r--r--www/wiki/extensions/MultimediaViewer/gitinfo.json1
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/af.json103
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/ais.json10
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/ar.json133
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/as.json18
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/ast.json127
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/atj.json9
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/av.json8
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/az.json10
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/bcl.json10
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/be-tarask.json46
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/be.json9
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/bg.json21
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/bgn.json9
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/bn.json130
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/br.json44
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/bs.json80
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/ca.json83
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/ce.json95
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/ckb.json51
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/cs.json139
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/cu.json8
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/cy.json27
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/da.json20
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/de-ch.json8
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/de-formal.json10
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/de.json137
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/diq.json12
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/dsb.json20
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/el.json99
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/eml.json8
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/en-gb.json11
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/en.json156
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/eo.json103
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/es-formal.json57
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/es.json138
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/et.json116
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/eu.json28
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/fa.json126
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/fi.json87
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/fr.json142
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/frr.json9
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/fy.json15
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/gd.json123
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/gl.json131
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/gu.json8
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/he.json138
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/hi.json25
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/hr.json135
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/hsb.json81
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/hu.json114
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/hy.json9
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/ia.json10
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/id.json22
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/ilo.json71
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/inh.json9
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/io.json10
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/is.json82
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/it.json137
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/ja.json107
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/jv.json36
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/ka.json58
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/kk-cyrl.json91
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/km.json8
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/kn.json49
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/ko.json77
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/krc.json17
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/ksh.json63
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/ku-latn.json10
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/lb.json91
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/lki.json10
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/lrc.json8
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/lt.json101
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/lv.json69
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/lzh.json14
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/mg.json8
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/mk.json137
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/ml.json144
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/mn.json10
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/mr.json16
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/ms.json24
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/mwl.json12
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/my.json10
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/nan.json8
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/nap.json11
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/nb.json131
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/nds-nl.json11
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/ne.json14
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/nl.json107
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/nn.json92
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/oc.json42
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/om.json9
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/or.json55
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/pa.json10
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/pl.json110
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/ps.json44
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/pt-br.json161
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/pt.json132
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/qqq.json166
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/qu.json33
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/ro.json118
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/roa-tara.json11
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/ru.json122
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/sa.json18
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/sco.json44
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/sd.json12
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/sdc.json8
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/sgs.json25
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/si.json18
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/sk.json71
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/sl.json50
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/sq.json8
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/sr-ec.json111
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/sr-el.json93
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/su.json8
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/sv.json119
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/sw.json14
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/ta.json12
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/tcy.json9
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/te.json60
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/tg-cyrl.json8
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/th.json27
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/tl.json12
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/tr.json80
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/tt-cyrl.json77
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/uk.json133
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/ur.json70
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/vi.json138
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/vo.json9
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/wa.json8
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/yi.json55
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/zh-hans.json159
-rw-r--r--www/wiki/extensions/MultimediaViewer/i18n/zh-hant.json116
-rwxr-xr-xwww/wiki/extensions/MultimediaViewer/importml.sh25
-rw-r--r--www/wiki/extensions/MultimediaViewer/includes/MultimediaViewerHooks.php349
-rw-r--r--www/wiki/extensions/MultimediaViewer/jsduck.categories.json104
-rw-r--r--www/wiki/extensions/MultimediaViewer/jsduck.external.js87
-rw-r--r--www/wiki/extensions/MultimediaViewer/jsduck.json12
-rw-r--r--www/wiki/extensions/MultimediaViewer/package.json18
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/jquery.hashchange/jquery.hashchange.js260
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/img/down.svg4
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/img/expand.svg5
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/img/gear.svg4
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/img/x_gray.svg4
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/logging/mmv.logging.ActionLogger.js192
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/logging/mmv.logging.Api.js58
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/logging/mmv.logging.AttributionLogger.js73
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/logging/mmv.logging.DimensionLogger.js81
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/logging/mmv.logging.DurationLogger.js162
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/logging/mmv.logging.Logger.js160
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/logging/mmv.logging.PerformanceLogger.js455
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/logging/mmv.logging.ViewLogger.js178
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.Config.js272
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.EmbedFileFormatter.js251
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.HtmlUtils.js269
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.ThumbnailWidthCalculator.js172
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.base.js34
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.bootstrap.autostart.js34
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.bootstrap.js631
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.bootstrap.less69
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.globals.less31
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.head.js59
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.js1031
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.lightboximage.js63
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.lightboxinterface.js509
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.lightboxinterface.less105
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.mixins.less60
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/model/mmv.model.EmbedFileInfo.js53
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/model/mmv.model.Image.js343
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/model/mmv.model.IwTitle.js80
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/model/mmv.model.License.js144
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/model/mmv.model.Repo.js209
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/model/mmv.model.TaskQueue.js141
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/model/mmv.model.Thumbnail.js48
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/model/mmv.model.ThumbnailWidth.js82
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/model/mmv.model.js20
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/provider/mmv.provider.Api.js199
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/provider/mmv.provider.FileRepoInfo.js63
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/provider/mmv.provider.GuessedThumbnailInfo.js307
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/provider/mmv.provider.Image.js153
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/provider/mmv.provider.ImageInfo.js117
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/provider/mmv.provider.ThumbnailInfo.js89
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/routing/mmv.routing.MainFileRoute.js30
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/routing/mmv.routing.Route.js28
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/routing/mmv.routing.Router.js197
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/routing/mmv.routing.ThumbnailRoute.js36
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/routing/mmv.routing.js20
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/checker.pngbin0 -> 81 bytes
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/cc.svg5
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/commons_white.svg7
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/ellipsis_darkgray.svg7
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/ellipsis_gray.svg7
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/ellipsis_lightgray.svg7
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/error-media-icon.svg7
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/file.svg5
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/gear.svg5
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/grayscale.svg6
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/icon_mmv.svg7
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/icon_page.svg7
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/license.svg4
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/link-hover.svg4
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/link.svg4
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/location.svg4
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/mw-close.svg5
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/mw-defullscreen-ltr.svg7
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/mw-defullscreen-rtl.svg7
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/mw-download.svg5
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/mw-fullscreen-ltr.svg5
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/mw-fullscreen-rtl.svg5
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/next-ltr.svg5
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/next-rtl.svg5
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/open.svg5
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/page.svg4
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/pasting-hover.svg7
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/pasting.svg7
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/pd.svg5
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/prev-ltr.svg5
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/prev-rtl.svg5
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/restrict-2257.svg4
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/restrict-aus-reserve.svg4
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/restrict-communist.svg4
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/restrict-costume.svg4
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/restrict-currency.svg4
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/restrict-default.svg4
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/restrict-design.svg4
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/restrict-fan-art.svg4
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/restrict-ihl.svg4
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/restrict-insignia.svg5
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/restrict-ita-mibac.svg4
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/restrict-nazi.svg4
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/restrict-personality.svg4
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/time.svg4
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/use-ltr.svg5
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/use-rtl.svg5
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/user-ltr.svg7
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/user-rtl.svg7
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/x_gray.svg4
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/x_white.svg4
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.canvas.js467
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.canvas.less89
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.canvasButtons.js285
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.canvasButtons.less142
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.description.js69
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.dialog.js258
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.dialog.less45
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.download.dialog.js128
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.download.dialog.less34
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.download.js20
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.download.pane.js429
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.download.pane.less206
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.js268
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.metadataPanel.js886
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.metadataPanel.less392
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.metadataPanelScroller.js247
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.metadataPanelScroller.less69
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.permission.js173
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.permission.less81
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.progressBar.js93
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.progressBar.less46
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.reuse.dialog.js264
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.reuse.dialog.less72
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.reuse.embed.js542
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.reuse.embed.less77
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.reuse.share.js166
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.reuse.share.less49
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.reuse.shareembed.less8
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.reuse.tab.js57
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.stripeButtons.js136
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.stripeButtons.less68
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.tipsyDialog.js202
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.tipsyDialog.less56
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.truncatableTextField.js240
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.truncatableTextField.less74
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.utils.js210
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.viewingOptions.js397
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.viewingOptions.less160
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/browser/LocalSettings.php2
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/browser/README.md1
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/browser/ci.yml28
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/browser/environments.yml42
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/browser/features/mmv.download.feature59
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/browser/features/mmv.navigation.feature23
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/browser/features/mmv.options.feature44
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/browser/features/mmv.performance.feature42
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/browser/features/step_definitions/mmv_download_steps.rb101
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/browser/features/step_definitions/mmv_navigation_steps.rb44
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/browser/features/step_definitions/mmv_options_steps.rb69
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/browser/features/step_definitions/mmv_performance_steps.rb48
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/browser/features/step_definitions/mmv_steps.rb174
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/browser/features/support/env.rb3
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/browser/features/support/pages/commons_page.rb51
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/browser/features/support/pages/e2e_test_page.rb87
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/browser/samples/MediaViewerE2ETest.wikitext14
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/phan/config.php19
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/logging/mmv.logging.ActionLogger.test.js48
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/logging/mmv.logging.AttributionLogger.test.js22
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/logging/mmv.logging.DimensionLogger.test.js17
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/logging/mmv.logging.DurationLogger.test.js218
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/logging/mmv.logging.PerformanceLogger.test.js341
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/logging/mmv.logging.ViewLogger.test.js87
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.Config.test.js203
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.EmbedFileFormatter.test.js293
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.HtmlUtils.test.js192
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.ThumbnailWidthCalculator.test.js149
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.bootstrap.test.js582
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.lightboximage.test.js10
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.lightboxinterface.test.js306
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.test.js706
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.testhelpers.js174
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.EmbedFileInfo.test.js40
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.Image.test.js151
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.IwTitle.test.js43
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.License.test.js161
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.Repo.test.js100
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.TaskQueue.test.js276
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.test.js58
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/provider/mmv.provider.Api.test.js270
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/provider/mmv.provider.FileRepoInfo.test.js126
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/provider/mmv.provider.GuessedThumbnailInfo.test.js280
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/provider/mmv.provider.Image.test.js200
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/provider/mmv.provider.ImageInfo.test.js241
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/provider/mmv.provider.ThumbnailInfo.test.js165
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/routing/mmv.routing.MainFileRoute.test.js24
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/routing/mmv.routing.Router.test.js232
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/routing/mmv.routing.ThumbnailRoute.test.js32
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.canvas.test.js287
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.canvasButtons.test.js36
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.description.test.js42
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.download.pane.test.js164
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.metadataPanel.test.js207
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.metadataPanelScroller.test.js232
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.permission.test.js112
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.progressBar.test.js77
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.reuse.dialog.test.js250
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.reuse.embed.test.js398
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.reuse.share.test.js95
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.reuse.tab.test.js43
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.reuse.utils.test.js117
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.stripeButtons.test.js76
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.test.js109
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.tipsyDialog.test.js68
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.truncatableTextField.test.js64
-rw-r--r--www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.viewingOptions.test.js139
-rw-r--r--www/wiki/extensions/MultimediaViewer/version4
-rw-r--r--www/wiki/extensions/MultimediaViewer/viewer-ltr.svg24
-rw-r--r--www/wiki/extensions/MultimediaViewer/viewer-rtl.svg24
363 files changed, 33787 insertions, 0 deletions
diff --git a/www/wiki/extensions/MultimediaViewer/.eslintrc.json b/www/wiki/extensions/MultimediaViewer/.eslintrc.json
new file mode 100644
index 00000000..488ab054
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/.eslintrc.json
@@ -0,0 +1,15 @@
+{
+ "extends": "wikimedia",
+ "env": {
+ "browser": true,
+ "jquery": true,
+ "qunit": true
+ },
+ "globals": {
+ "OO": false,
+ "mediaWiki": false
+ },
+ "rules": {
+ "dot-notation": [ "error", { "allowKeywords": true } ]
+ }
+}
diff --git a/www/wiki/extensions/MultimediaViewer/.gitignore b/www/wiki/extensions/MultimediaViewer/.gitignore
new file mode 100644
index 00000000..574eea50
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/.gitignore
@@ -0,0 +1,5 @@
+docs/
+node_modules/
+vendor/
+/composer.lock
+tests/phan/issues
diff --git a/www/wiki/extensions/MultimediaViewer/.gitreview b/www/wiki/extensions/MultimediaViewer/.gitreview
new file mode 100644
index 00000000..b156b5f0
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/.gitreview
@@ -0,0 +1,6 @@
+[gerrit]
+host=gerrit.wikimedia.org
+port=29418
+project=mediawiki/extensions/MultimediaViewer.git
+track=1
+defaultrebase=0 \ No newline at end of file
diff --git a/www/wiki/extensions/MultimediaViewer/.phpcs.xml b/www/wiki/extensions/MultimediaViewer/.phpcs.xml
new file mode 100644
index 00000000..93acceed
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/.phpcs.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0"?>
+<ruleset>
+ <rule ref="./vendor/mediawiki/mediawiki-codesniffer/MediaWiki">
+ <exclude name="MediaWiki.Commenting.FunctionComment.MissingDocumentationPublic" />
+ </rule>
+ <file>.</file>
+ <arg name="extensions" value="php,php5,inc" />
+ <arg name="encoding" value="UTF-8" />
+</ruleset>
diff --git a/www/wiki/extensions/MultimediaViewer/.rubocop.yml b/www/wiki/extensions/MultimediaViewer/.rubocop.yml
new file mode 100644
index 00000000..f4e4cba6
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/.rubocop.yml
@@ -0,0 +1,24 @@
+inherit_from: .rubocop_todo.yml
+
+AllCops:
+ # Only enforce rules that have an entry in the style guide
+ StyleGuideCopsOnly: true
+
+# uncomment when the offense is fixed
+# Metrics/LineLength:
+# Max: 100
+
+Metrics/MethodLength:
+ Enabled: false
+
+Style/Alias:
+ Enabled: false
+
+Style/SignalException:
+ Enabled: false
+
+Style/StringLiterals:
+ EnforcedStyle: single_quotes
+
+Style/TrivialAccessors:
+ ExactNameMatch: true
diff --git a/www/wiki/extensions/MultimediaViewer/.rubocop_todo.yml b/www/wiki/extensions/MultimediaViewer/.rubocop_todo.yml
new file mode 100644
index 00000000..f90453c4
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/.rubocop_todo.yml
@@ -0,0 +1,40 @@
+# This configuration was generated by
+# `rubocop --auto-gen-config`
+# on 2017-12-04 16:52:05 +0100 using RuboCop version 0.51.0.
+# The point is for the user to remove these configuration records
+# one by one as the offenses are removed from the code base.
+# Note that changes in the inspected code, or installation of new
+# versions of RuboCop, may require this file to be generated again.
+
+# Offense count: 1
+Lint/RescueWithoutErrorClass:
+ Exclude:
+ - 'tests/browser/features/step_definitions/mmv_steps.rb'
+
+# Offense count: 71
+# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
+# URISchemes: http, https
+Metrics/LineLength:
+ Max: 249
+
+# Offense count: 2
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+# SupportedStyles: lowercase, uppercase
+Naming/HeredocDelimiterCase:
+ Exclude:
+ - 'tests/browser/features/support/pages/commons_page.rb'
+
+# Offense count: 4
+# Cop supports --auto-correct.
+Style/Encoding:
+ Exclude:
+ - 'tests/browser/features/step_definitions/mmv_download_steps.rb'
+ - 'tests/browser/features/step_definitions/mmv_navigation_steps.rb'
+ - 'tests/browser/features/step_definitions/mmv_options_steps.rb'
+ - 'tests/browser/features/step_definitions/mmv_steps.rb'
+
+# Offense count: 3
+# Configuration parameters: AllowedVariables.
+Style/GlobalVars:
+ Exclude:
+ - 'tests/browser/features/step_definitions/mmv_performance_steps.rb'
diff --git a/www/wiki/extensions/MultimediaViewer/.stylelintrc.json b/www/wiki/extensions/MultimediaViewer/.stylelintrc.json
new file mode 100644
index 00000000..cae35a02
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/.stylelintrc.json
@@ -0,0 +1,6 @@
+{
+ "extends": "stylelint-config-wikimedia",
+ "rules": {
+ "no-descending-specificity": null
+ }
+}
diff --git a/www/wiki/extensions/MultimediaViewer/AUTHORS b/www/wiki/extensions/MultimediaViewer/AUTHORS
new file mode 100644
index 00000000..f6bb5b1d
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/AUTHORS
@@ -0,0 +1,24 @@
+* Mark Holmquist <mtraceur@member.fsf.org>
+* Gilles Dubuc <gdubuc@wikimedia.org>
+* Gergő Tisza <tgr.huwiki@gmail.com>
+* Aaron Arcos <aarcos.wiki@gmail.com>
+* Zeljko Filipin <zeljko.filipin@gmail.com>
+* Pau Giner <pau.giner@gmail.com>
+* theopolisme <theopolismewiki@gmail.com>
+* MatmaRex <matma.rex@gmail.com>
+* apsdehal <amanpreet.iitr2013@gmail.com>
+* vldandrew <vldandrew@gmail.com>
+* Ebrahim Byagowi <ebrahim@gnu.org>
+* Dereckson <dereckson@espace-win.org>
+* Brion VIBBER <brion@wikimedia.org>
+* Yuki Shira <shirayuking@gmail.com>
+* Yaroslav Melnychuk <yaroslavmelnuchuk@gmail.com>
+* tonythomas01 <01tonythomas@gmail.com>
+* raymond <raimond.spekking@gmail.com>
+* Kunal Mehta <legoktm@gmail.com>
+* Jeff Hall <jhall@wikimedia.org>
+* Christian Aistleitner <christian@quelltextlich.at>
+* Amir E. Aharoni <amir.aharoni@mail.huji.ac.il>
+
+Generated with git log --format='%aN <%aE>' | awk '{arr[$0]++} END{for (i in arr){print arr[i], i;}}' | sort -rn | cut -d\ -f2-
+Manually pruned for bots and duplicates
diff --git a/www/wiki/extensions/MultimediaViewer/CODE_OF_CONDUCT.md b/www/wiki/extensions/MultimediaViewer/CODE_OF_CONDUCT.md
new file mode 100644
index 00000000..d8e5d087
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/CODE_OF_CONDUCT.md
@@ -0,0 +1 @@
+The development of this software is covered by a [Code of Conduct](https://www.mediawiki.org/wiki/Code_of_Conduct).
diff --git a/www/wiki/extensions/MultimediaViewer/COPYING b/www/wiki/extensions/MultimediaViewer/COPYING
new file mode 100644
index 00000000..d159169d
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/COPYING
@@ -0,0 +1,339 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ <signature of Ty Coon>, 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/www/wiki/extensions/MultimediaViewer/Gemfile b/www/wiki/extensions/MultimediaViewer/Gemfile
new file mode 100644
index 00000000..591fd6bc
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/Gemfile
@@ -0,0 +1,5 @@
+source 'https://rubygems.org'
+
+gem 'mediawiki_selenium', '~> 1.8'
+gem 'rake', '~> 11.1', '>= 11.1.1'
+gem 'rubocop', '~> 0.51.0', require: false
diff --git a/www/wiki/extensions/MultimediaViewer/Gemfile.lock b/www/wiki/extensions/MultimediaViewer/Gemfile.lock
new file mode 100644
index 00000000..1b16d93e
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/Gemfile.lock
@@ -0,0 +1,113 @@
+GEM
+ remote: https://rubygems.org/
+ specs:
+ ast (2.3.0)
+ builder (3.2.3)
+ childprocess (0.6.2)
+ ffi (~> 1.0, >= 1.0.11)
+ cucumber (1.3.20)
+ builder (>= 2.1.2)
+ diff-lcs (>= 1.1.3)
+ gherkin (~> 2.12)
+ multi_json (>= 1.7.5, < 2.0)
+ multi_test (>= 0.1.2)
+ data_magic (1.0)
+ faker (>= 1.1.2)
+ yml_reader (>= 0.6)
+ diff-lcs (1.3)
+ domain_name (0.5.20170223)
+ unf (>= 0.0.5, < 1.0.0)
+ faker (1.7.3)
+ i18n (~> 0.5)
+ faraday (0.11.0)
+ multipart-post (>= 1.2, < 3)
+ faraday-cookie_jar (0.0.6)
+ faraday (>= 0.7.4)
+ http-cookie (~> 1.0.0)
+ faraday_middleware (0.11.0.1)
+ faraday (>= 0.7.4, < 1.0)
+ ffi (1.9.17)
+ gherkin (2.12.2)
+ multi_json (~> 1.3)
+ headless (2.3.1)
+ http-cookie (1.0.3)
+ domain_name (~> 0.5)
+ i18n (0.8.1)
+ json (2.0.3)
+ mediawiki_api (0.7.1)
+ faraday (~> 0.9, >= 0.9.0)
+ faraday-cookie_jar (~> 0.0, >= 0.0.6)
+ faraday_middleware (~> 0.10, >= 0.10.0)
+ mediawiki_selenium (1.8.0)
+ cucumber (~> 1.3, >= 1.3.20)
+ headless (~> 2.0, >= 2.1.0)
+ json (~> 2.0, >= 2.0.2)
+ mediawiki_api (~> 0.7, >= 0.7.0)
+ page-object (~> 2.0)
+ rest-client (~> 1.6, >= 1.6.7)
+ rspec-core (~> 2.14, >= 2.14.4)
+ rspec-expectations (~> 2.14, >= 2.14.4)
+ selenium-webdriver (~> 3.1.0)
+ syntax (~> 1.2, >= 1.2.0)
+ thor (~> 0.19, >= 0.19.1)
+ mime-types (2.99.3)
+ multi_json (1.12.1)
+ multi_test (0.1.2)
+ multipart-post (2.0.0)
+ net-http-persistent (2.9.4)
+ netrc (0.11.0)
+ page-object (2.0.0)
+ net-http-persistent (~> 2.9.4)
+ page_navigation (>= 0.9)
+ selenium-webdriver (~> 3.0)
+ watir (~> 6.0)
+ page_navigation (0.10)
+ data_magic (>= 0.22)
+ parallel (1.12.0)
+ parser (2.4.0.2)
+ ast (~> 2.3)
+ powerpack (0.1.1)
+ rainbow (2.2.2)
+ rake
+ rake (11.1.1)
+ rest-client (1.8.0)
+ http-cookie (>= 1.0.2, < 2.0)
+ mime-types (>= 1.16, < 3.0)
+ netrc (~> 0.7)
+ rspec-core (2.99.2)
+ rspec-expectations (2.99.2)
+ diff-lcs (>= 1.1.3, < 2.0)
+ rubocop (0.51.0)
+ parallel (~> 1.10)
+ parser (>= 2.3.3.1, < 3.0)
+ powerpack (~> 0.1)
+ rainbow (>= 2.2.2, < 3.0)
+ ruby-progressbar (~> 1.7)
+ unicode-display_width (~> 1.0, >= 1.0.1)
+ ruby-progressbar (1.9.0)
+ rubyzip (1.2.1)
+ selenium-webdriver (3.1.0)
+ childprocess (~> 0.5)
+ rubyzip (~> 1.0)
+ websocket (~> 1.0)
+ syntax (1.2.1)
+ thor (0.19.4)
+ unf (0.1.4)
+ unf_ext
+ unf_ext (0.0.7.2)
+ unicode-display_width (1.3.0)
+ watir (6.2.0)
+ selenium-webdriver (~> 3.0)
+ websocket (1.2.4)
+ yml_reader (0.7)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ mediawiki_selenium (~> 1.8)
+ rake (~> 11.1, >= 11.1.1)
+ rubocop (~> 0.51.0)
+
+BUNDLED WITH
+ 1.16.0
diff --git a/www/wiki/extensions/MultimediaViewer/Gruntfile.js b/www/wiki/extensions/MultimediaViewer/Gruntfile.js
new file mode 100644
index 00000000..fe6aacc5
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/Gruntfile.js
@@ -0,0 +1,72 @@
+/* eslint-env node */
+
+module.exports = function ( grunt ) {
+ var conf = grunt.file.readJSON( 'extension.json' );
+
+ grunt.loadNpmTasks( 'grunt-banana-checker' );
+ grunt.loadNpmTasks( 'grunt-eslint' );
+ grunt.loadNpmTasks( 'grunt-jsonlint' );
+ grunt.loadNpmTasks( 'grunt-stylelint' );
+ grunt.loadNpmTasks( 'grunt-svgmin' );
+
+ grunt.initConfig( {
+ banana: conf.MessagesDirs,
+ jsonlint: {
+ all: [
+ '**/*.json',
+ '!node_modules/**',
+ '!vendor/**'
+ ]
+ },
+ eslint: {
+ all: [
+ '*.js',
+ 'resources/mmv/**/*.js',
+ 'tests/**/*.js'
+ ]
+ },
+ stylelint: {
+ options: {
+ syntax: 'less'
+ },
+ src: 'resources/mmv/**/*.{css,less}'
+ },
+ // Image Optimization
+ svgmin: {
+ options: {
+ js2svg: {
+ pretty: true
+ },
+ plugins: [ {
+ cleanupIDs: false
+ }, {
+ removeDesc: false
+ }, {
+ removeRasterImages: true
+ }, {
+ removeTitle: false
+ }, {
+ removeViewBox: false
+ }, {
+ removeXMLProcInst: false
+ }, {
+ sortAttrs: true
+ } ]
+ },
+ all: {
+ files: [ {
+ expand: true,
+ cwd: 'resources',
+ src: [
+ '**/*.svg'
+ ],
+ dest: 'resources/',
+ ext: '.svg'
+ } ]
+ }
+ }
+ } );
+
+ grunt.registerTask( 'test', [ 'eslint', 'stylelint', 'svgmin', 'jsonlint', 'banana' ] );
+ grunt.registerTask( 'default', 'test' );
+};
diff --git a/www/wiki/extensions/MultimediaViewer/MultimediaViewer.php b/www/wiki/extensions/MultimediaViewer/MultimediaViewer.php
new file mode 100644
index 00000000..fbeaa54f
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/MultimediaViewer.php
@@ -0,0 +1,132 @@
+<?php
+/**
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @file
+ * @ingroup extensions
+ * @author Mark Holmquist <mtraceur@member.fsf.org>
+ * @copyright Copyright © 2013, Mark Holmquist
+ */
+
+if ( function_exists( 'wfLoadExtension' ) ) {
+ wfLoadExtension( 'MultimediaViewer' );
+ // Keep i18n globals so mergeMessageFileList.php doesn't break
+ $wgMessagesDirs['MultimediaViewer'] = __DIR__ . '/i18n';
+ /* wfWarn(
+ 'Deprecated PHP entry point used for MultimediaViewer extension. ' .
+ 'Please use wfLoadExtension instead, ' .
+ 'see https://www.mediawiki.org/wiki/Extension_registration for more details.'
+ ); */
+ return;
+} else {
+ die( 'This version of the MultimediaViewer extension requires MediaWiki 1.25+' );
+}
+
+// The following is for the purposes of IDEs and documentation. It is not
+// executed.
+
+/**
+ * If set, records image load network performance via
+ * EventLogging once per this many requests. False if unset.
+ *
+ * @var int|bool
+ */
+$wgMediaViewerNetworkPerformanceSamplingFactor = false;
+
+/**
+ * If set, records loading times via EventLogging. A value of 1000 means there will be an
+ * 1:1000 chance to log the duration event.
+ * False if unset.
+ * @var int|bool
+ */
+$wgMediaViewerDurationLoggingSamplingFactor = false;
+
+/**
+ * If set, records loading times via EventLogging with factor specific to loggedin users.
+ * A value of 1000 means there will be an 1:1000 chance to log the duration event.
+ * False if unset.
+ * @var int|bool
+ */
+$wgMediaViewerDurationLoggingLoggedinSamplingFactor = false;
+
+/**
+ * If set, records whether image attribution data was available.
+ * A value of 1000 means there will be an 1:1000 chance to log the attribution event.
+ * False if unset.
+ * @var int|bool
+ */
+$wgMediaViewerAttributionLoggingSamplingFactor = false;
+
+/**
+ * If set, records whether image dimension data was available.
+ * A value of 1000 means there will be an 1:1000 chance to log the dimension event.
+ * False if unset.
+ * @var int|bool
+ */
+$wgMediaViewerDimensionLoggingSamplingFactor = false;
+
+/**
+ * If set, records user actions via EventLogging and applies a sampling factor according
+ * to the map. A "default" key in the map must be set.
+ * False if unset.
+ * @var array|bool
+ */
+$wgMediaViewerActionLoggingSamplingFactorMap = false;
+
+/**
+ * If set, Media Viewer will try to use BetaFeatures. False if unset.
+ * @var bool
+ */
+$wgMediaViewerIsInBeta = false;
+
+/**
+ * When this is enabled, MediaViewer will try to guess image URLs instead of making an
+ * imageinfo API to get them from the server. This speeds up image loading, but will
+ * result in 404s when $wgGenerateThumbnailOnParse (so the thumbnails are only generated
+ * as a result of the API request). MediaViewer will catch such 404 errors and fall back
+ * to the API request, but depending on how the site is set up, the 404 might get cached,
+ * or redirected, causing the image load to fail. The safe way to use URL guessing is
+ * with a 404 handler: https://www.mediawiki.org/wiki/Manual:Thumb.php#404_Handler
+ *
+ * @var bool
+ */
+$wgMediaViewerUseThumbnailGuessing = false;
+
+/**
+ * If trueish, and $wgMediaViewerIsInBeta is unset,
+ * Media Viewer will be turned on by default.
+ * @var bool
+ */
+$wgMediaViewerEnableByDefault = true;
+
+/**
+ * Overrides $wgMediaViewerEnableByDefault for anonymous users. If
+ * set to null, will fall back to value of $wgMediaViewerEnableByDefault
+ * @var bool|null
+ */
+$wgMediaViewerEnableByDefaultForAnonymous = null;
+
+/**
+ * If set, adds a query parameter to image requests made by Media Viewer
+ * @var string|bool
+ */
+$wgMediaViewerImageQueryParameter = false;
+
+/**
+ * If set, records a virtual view via the provided beacon URI.
+ * @var string|bool
+ */
+$wgMediaViewerRecordVirtualViewBeaconURI = false;
diff --git a/www/wiki/extensions/MultimediaViewer/Rakefile b/www/wiki/extensions/MultimediaViewer/Rakefile
new file mode 100644
index 00000000..2877bf69
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/Rakefile
@@ -0,0 +1,17 @@
+require 'bundler/setup'
+
+require 'rubocop/rake_task'
+RuboCop::RakeTask.new(:rubocop) do |task|
+ # if you use mediawiki-vagrant, rubocop will by default use it's .rubocop.yml
+ # the next line makes it explicit that you want .rubocop.yml from the directory
+ # where `bundle exec rake` is executed
+ task.options = ['-c', '.rubocop.yml']
+end
+
+require 'mediawiki_selenium/rake_task'
+MediawikiSelenium::RakeTask.new
+
+task default: [:test]
+
+desc 'Run all build/tests commands (CI entry point)'
+task test: [:rubocop]
diff --git a/www/wiki/extensions/MultimediaViewer/composer.json b/www/wiki/extensions/MultimediaViewer/composer.json
new file mode 100644
index 00000000..0107a5d4
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/composer.json
@@ -0,0 +1,20 @@
+{
+ "require-dev": {
+ "jakub-onderka/php-parallel-lint": "1.0.0",
+ "jakub-onderka/php-console-highlighter": "0.3.2",
+ "mediawiki/mediawiki-codesniffer": "18.0.0",
+ "mediawiki/minus-x": "0.3.1",
+ "mediawiki/mediawiki-phan-config": "0.2.0"
+ },
+ "scripts": {
+ "fix": [
+ "phpcbf",
+ "minus-x fix ."
+ ],
+ "test": [
+ "parallel-lint . --exclude vendor --exclude node_modules",
+ "phpcs -p -s",
+ "minus-x check ."
+ ]
+ }
+}
diff --git a/www/wiki/extensions/MultimediaViewer/extension.json b/www/wiki/extensions/MultimediaViewer/extension.json
new file mode 100644
index 00000000..0ae6b5ad
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/extension.json
@@ -0,0 +1,444 @@
+{
+ "name": "MultimediaViewer",
+ "author": [
+ "MarkTraceur (Mark Holmquist)",
+ "Gilles Dubuc",
+ "Gergő Tisza",
+ "Aaron Arcos",
+ "Zeljko Filipin",
+ "Pau Giner",
+ "theopolisme",
+ "MatmaRex",
+ "apsdehal",
+ "vldandrew",
+ "Ebrahim Byagowi",
+ "Dereckson",
+ "Brion VIBBER",
+ "Yuki Shira",
+ "Yaroslav Melnychuk",
+ "tonythomas01",
+ "Raimond Spekking",
+ "Kunal Mehta",
+ "Jeff Hall",
+ "Christian Aistleitner",
+ "Amir E. Aharoni"
+ ],
+ "url": "https://mediawiki.org/wiki/Extension:MultimediaViewer",
+ "descriptionmsg": "multimediaviewer-desc",
+ "license-name": "GPL-2.0-or-later",
+ "type": "other",
+ "ExtensionFunctions": [
+ "MultimediaViewerHooks::onExtensionFunctions"
+ ],
+ "MessagesDirs": {
+ "MultimediaViewer": [
+ "i18n"
+ ]
+ },
+ "AutoloadClasses": {
+ "MultimediaViewerHooks": "includes/MultimediaViewerHooks.php"
+ },
+ "ResourceModules": {
+ "mmv": {
+ "scripts": [
+ "mmv/logging/mmv.logging.Api.js",
+ "mmv/logging/mmv.logging.AttributionLogger.js",
+ "mmv/logging/mmv.logging.DimensionLogger.js",
+ "mmv/logging/mmv.logging.ViewLogger.js",
+ "mmv/logging/mmv.logging.PerformanceLogger.js",
+ "mmv/routing/mmv.routing.js",
+ "mmv/routing/mmv.routing.Route.js",
+ "mmv/routing/mmv.routing.ThumbnailRoute.js",
+ "mmv/routing/mmv.routing.MainFileRoute.js",
+ "mmv/routing/mmv.routing.Router.js",
+ "mmv/model/mmv.model.js",
+ "mmv/model/mmv.model.IwTitle.js",
+ "mmv/model/mmv.model.License.js",
+ "mmv/model/mmv.model.Image.js",
+ "mmv/model/mmv.model.Repo.js",
+ "mmv/model/mmv.model.Thumbnail.js",
+ "mmv/model/mmv.model.TaskQueue.js",
+ "mmv/model/mmv.model.ThumbnailWidth.js",
+ "mmv/mmv.lightboximage.js",
+ "mmv/provider/mmv.provider.Api.js",
+ "mmv/provider/mmv.provider.ImageInfo.js",
+ "mmv/provider/mmv.provider.FileRepoInfo.js",
+ "mmv/provider/mmv.provider.ThumbnailInfo.js",
+ "mmv/provider/mmv.provider.GuessedThumbnailInfo.js",
+ "mmv/provider/mmv.provider.Image.js",
+ "mmv/mmv.ThumbnailWidthCalculator.js",
+ "mmv/ui/mmv.ui.js",
+ "mmv/ui/mmv.ui.dialog.js",
+ "mmv/ui/mmv.ui.reuse.dialog.js",
+ "mmv/ui/mmv.ui.download.js",
+ "mmv/ui/mmv.ui.download.dialog.js",
+ "mmv/ui/mmv.ui.description.js",
+ "mmv/ui/mmv.ui.viewingOptions.js",
+ "mmv/ui/mmv.ui.canvas.js",
+ "mmv/ui/mmv.ui.canvasButtons.js",
+ "mmv/ui/mmv.ui.permission.js",
+ "mmv/ui/mmv.ui.progressBar.js",
+ "mmv/ui/mmv.ui.stripeButtons.js",
+ "mmv/ui/mmv.ui.truncatableTextField.js",
+ "mmv/ui/mmv.ui.metadataPanel.js",
+ "mmv/ui/mmv.ui.metadataPanelScroller.js",
+ "mmv/mmv.lightboxinterface.js",
+ "mmv/mmv.js"
+ ],
+ "styles": [
+ "mmv/ui/mmv.ui.dialog.less",
+ "mmv/ui/mmv.ui.reuse.dialog.less",
+ "mmv/ui/mmv.ui.download.dialog.less",
+ "mmv/ui/mmv.ui.viewingOptions.less",
+ "mmv/ui/mmv.ui.canvas.less",
+ "mmv/ui/mmv.ui.canvasButtons.less",
+ "mmv/ui/mmv.ui.permission.less",
+ "mmv/ui/mmv.ui.progressBar.less",
+ "mmv/ui/mmv.ui.stripeButtons.less",
+ "mmv/ui/mmv.ui.truncatableTextField.less",
+ "mmv/ui/mmv.ui.metadataPanel.less",
+ "mmv/ui/mmv.ui.metadataPanelScroller.less",
+ "mmv/mmv.lightboxinterface.less"
+ ],
+ "dependencies": [
+ "mediawiki.api",
+ "mediawiki.Title",
+ "mediawiki.Uri",
+ "mediawiki.jqueryMsg",
+ "mediawiki.storage",
+ "oojs",
+ "jquery.fullscreen",
+ "jquery.hidpi",
+ "jquery.throttle-debounce",
+ "jquery.color",
+ "jquery.tipsy",
+ "mmv.bootstrap",
+ "mmv.head"
+ ],
+ "messages": [
+ "multimediaviewer-file-page",
+ "multimediaviewer-options-learn-more",
+ "multimediaviewer-options-dialog-header",
+ "multimediaviewer-option-submit-button",
+ "multimediaviewer-option-cancel-button",
+ "multimediaviewer-options-text-header",
+ "multimediaviewer-enable-alert",
+ "multimediaviewer-options-text-body",
+ "multimediaviewer-disable-confirmation-header",
+ "multimediaviewer-disable-confirmation-text",
+ "multimediaviewer-enable-dialog-header",
+ "multimediaviewer-enable-text-header",
+ "multimediaviewer-enable-submit-button",
+ "multimediaviewer-enable-confirmation-header",
+ "multimediaviewer-enable-confirmation-text",
+ "multimediaviewer-thumbnail-error",
+ "multimediaviewer-thumbnail-error-description",
+ "multimediaviewer-thumbnail-error-retry",
+ "multimediaviewer-report-issue-url",
+ "multimediaviewer-thumbnail-error-report",
+ "multimediaviewer-errorreport-privacywarning",
+ "multimediaviewer-download-link",
+ "multimediaviewer-reuse-link",
+ "multimediaviewer-options-tooltip",
+ "multimediaviewer-close-popup-text",
+ "multimediaviewer-fullscreen-popup-text",
+ "multimediaviewer-defullscreen-popup-text",
+ "multimediaviewer-next-image-alt-text",
+ "multimediaviewer-prev-image-alt-text",
+ "multimediaviewer-commons-subtitle",
+ "multimediaviewer-credit",
+ "multimediaviewer-credit-fallback",
+ "multimediaviewer-multiple-authors",
+ "multimediaviewer-multiple-authors-combine",
+ "multimediaviewer-datetime-created",
+ "multimediaviewer-datetime-uploaded",
+ "multimediaviewer-permission-link",
+ "multimediaviewer-permission-link-hide",
+ "multimediaviewer-restriction-2257",
+ "multimediaviewer-restriction-aus-reserve",
+ "multimediaviewer-restriction-communist",
+ "multimediaviewer-restriction-costume",
+ "multimediaviewer-restriction-currency",
+ "multimediaviewer-restriction-design",
+ "multimediaviewer-restriction-fan-art",
+ "multimediaviewer-restriction-ihl",
+ "multimediaviewer-restriction-insignia",
+ "multimediaviewer-restriction-ita-mibac",
+ "multimediaviewer-restriction-nazi",
+ "multimediaviewer-restriction-personality",
+ "multimediaviewer-restriction-trademarked",
+ "multimediaviewer-restriction-default",
+ "multimediaviewer-restriction-default-and-others",
+ "multimediaviewer-reuse-warning-deletion",
+ "multimediaviewer-reuse-warning-nonfree",
+ "multimediaviewer-reuse-warning-noattribution",
+ "multimediaviewer-reuse-warning-generic",
+ "multimediaviewer-geoloc-north",
+ "multimediaviewer-geoloc-east",
+ "multimediaviewer-geoloc-south",
+ "multimediaviewer-geoloc-west",
+ "multimediaviewer-geoloc-coord",
+ "multimediaviewer-geoloc-coords",
+ "multimediaviewer-geolocation",
+ "multimediaviewer-about-mmv",
+ "multimediaviewer-discuss-mmv",
+ "multimediaviewer-help-mmv",
+ "multimediaviewer-optout-mmv",
+ "multimediaviewer-optin-mmv",
+ "multimediaviewer-optout-pending-mmv",
+ "multimediaviewer-optin-pending-mmv",
+ "multimediaviewer-optout-help",
+ "multimediaviewer-optin-help",
+ "mypreferences",
+ "multimediaviewer-metadata-error",
+ "multimediaviewer-title-popup-text",
+ "multimediaviewer-credit-popup-text",
+ "multimediaviewer-title-popup-text-more",
+ "multimediaviewer-credit-popup-text-more",
+ "multimediaviewer-permission-title",
+ "multimediaviewer-permission-viewmore",
+ "multimediaviewer-description-page-button-text",
+ "multimediaviewer-description-page-popup-text",
+ "multimediaviewer-repository-local",
+ "multimediaviewer-license-cc-by-1.0",
+ "multimediaviewer-license-cc-sa-1.0",
+ "multimediaviewer-license-cc-by-sa-1.0",
+ "multimediaviewer-license-cc-by-2.0",
+ "multimediaviewer-license-cc-by-sa-2.0",
+ "multimediaviewer-license-cc-by-2.1",
+ "multimediaviewer-license-cc-by-sa-2.1",
+ "multimediaviewer-license-cc-by-2.5",
+ "multimediaviewer-license-cc-by-sa-2.5",
+ "multimediaviewer-license-cc-by-3.0",
+ "multimediaviewer-license-cc-by-sa-3.0",
+ "multimediaviewer-license-cc-by-4.0",
+ "multimediaviewer-license-cc-by-sa-4.0",
+ "multimediaviewer-license-cc-pd",
+ "multimediaviewer-license-cc-zero",
+ "multimediaviewer-license-pd",
+ "multimediaviewer-license-default"
+ ]
+ },
+ "mmv.ui.ondemandshareddependencies": {
+ "scripts": [
+ "mmv/model/mmv.model.EmbedFileInfo.js",
+ "mmv/mmv.EmbedFileFormatter.js",
+ "mmv/ui/mmv.ui.utils.js"
+ ],
+ "dependencies": [
+ "mmv.head",
+ "mmv",
+ "oojs",
+ "oojs-ui"
+ ],
+ "messages": [
+ "multimediaviewer-credit",
+ "multimediaviewer-text-embed-credit-text-bl",
+ "multimediaviewer-text-embed-credit-text-b",
+ "multimediaviewer-text-embed-credit-text-l",
+ "multimediaviewer-html-embed-credit-text-bl",
+ "multimediaviewer-html-embed-credit-text-b",
+ "multimediaviewer-html-embed-credit-text-l",
+ "multimediaviewer-html-embed-credit-link-text"
+ ]
+ },
+ "mmv.ui.download.pane": {
+ "scripts": [
+ "mmv/ui/mmv.ui.download.pane.js"
+ ],
+ "styles": [
+ "mmv/ui/mmv.ui.download.pane.less"
+ ],
+ "dependencies": [
+ "mediawiki.ui",
+ "mediawiki.ui.button",
+ "mmv",
+ "mmv.ui.ondemandshareddependencies",
+ "oojs",
+ "oojs-ui"
+ ],
+ "messages": [
+ "multimediaviewer-download-preview-link-title",
+ "multimediaviewer-download-original-button-name",
+ "multimediaviewer-download-small-button-name",
+ "multimediaviewer-download-medium-button-name",
+ "multimediaviewer-download-large-button-name",
+ "multimediaviewer-embed-dimensions",
+ "multimediaviewer-embed-dimensions-with-file-format",
+ "multimediaviewer-download-attribution-cta-header",
+ "multimediaviewer-download-optional-attribution-cta-header",
+ "multimediaviewer-download-attribution-cta",
+ "multimediaviewer-download-attribution-copy",
+ "multimediaviewer-attr-plain",
+ "multimediaviewer-attr-html"
+ ]
+ },
+ "mmv.ui.reuse.shareembed": {
+ "scripts": [
+ "mmv/ui/mmv.ui.reuse.tab.js",
+ "mmv/ui/mmv.ui.reuse.share.js",
+ "mmv/ui/mmv.ui.reuse.embed.js"
+ ],
+ "styles": [
+ "mmv/ui/mmv.ui.reuse.share.less",
+ "mmv/ui/mmv.ui.reuse.embed.less",
+ "mmv/ui/mmv.ui.reuse.shareembed.less"
+ ],
+ "dependencies": [
+ "jquery.tipsy",
+ "oojs",
+ "oojs-ui",
+ "mediawiki.user",
+ "mmv.ui.ondemandshareddependencies"
+ ],
+ "messages": [
+ "multimediaviewer-reuse-loading-placeholder",
+ "multimediaviewer-share-tab",
+ "multimediaviewer-share-explanation",
+ "multimediaviewer-link-to-file",
+ "multimediaviewer-link-to-page",
+ "multimediaviewer-reuse-loading-placeholder",
+ "multimediaviewer-reuse-copy-share",
+ "multimediaviewer-reuse-copy-embed",
+ "multimediaviewer-embed-tab",
+ "multimediaviewer-embed-html",
+ "multimediaviewer-embed-wt",
+ "multimediaviewer-embed-explanation",
+ "multimediaviewer-embed-byline",
+ "multimediaviewer-embed-license",
+ "multimediaviewer-embed-via",
+ "multimediaviewer-default-embed-dimensions",
+ "multimediaviewer-original-embed-dimensions",
+ "multimediaviewer-large-embed-dimensions",
+ "multimediaviewer-medium-embed-dimensions",
+ "multimediaviewer-small-embed-dimensions",
+ "multimediaviewer-embed-dimensions",
+ "multimediaviewer-embed-dimensions-separated"
+ ]
+ },
+ "mmv.ui.tipsyDialog": {
+ "scripts": [
+ "mmv/ui/mmv.ui.tipsyDialog.js"
+ ],
+ "styles": [
+ "mmv/ui/mmv.ui.tipsyDialog.less"
+ ],
+ "dependencies": [
+ "mmv",
+ "oojs",
+ "jquery.tipsy"
+ ]
+ },
+ "mmv.bootstrap": {
+ "scripts": [
+ "mmv/mmv.Config.js",
+ "mmv/mmv.HtmlUtils.js",
+ "mmv/mmv.bootstrap.js",
+ "mmv/logging/mmv.logging.Logger.js",
+ "mmv/logging/mmv.logging.ActionLogger.js",
+ "mmv/logging/mmv.logging.DurationLogger.js",
+ "jquery.hashchange/jquery.hashchange.js"
+ ],
+ "styles": [
+ "mmv/mmv.bootstrap.less"
+ ],
+ "dependencies": [
+ "mediawiki.api",
+ "mediawiki.api.options",
+ "mediawiki.ui.button",
+ "mediawiki.ui.icon",
+ "mediawiki.Title",
+ "mediawiki.user",
+ "mediawiki.storage",
+ "mmv.head",
+ "oojs"
+ ],
+ "messages": [
+ "multimediaviewer-view-expanded",
+ "multimediaviewer-view-config",
+ "multimediaviewer-disable-info-title",
+ "multimediaviewer-disable-info"
+ ]
+ },
+ "mmv.bootstrap.autostart": {
+ "scripts": [
+ "mmv/mmv.bootstrap.autostart.js"
+ ],
+ "dependencies": [
+ "mmv.head",
+ "mmv.bootstrap"
+ ]
+ },
+ "mmv.head": {
+ "scripts": [
+ "mmv/mmv.base.js",
+ "mmv/mmv.head.js"
+ ],
+ "dependencies": [
+ "mediawiki.user",
+ "mediawiki.storage"
+ ],
+ "position": "top"
+ }
+ },
+ "ResourceFileModulePaths": {
+ "localBasePath": "resources",
+ "remoteExtPath": "MultimediaViewer/resources"
+ },
+ "Hooks": {
+ "EventLoggingRegisterSchemas": [
+ "MultimediaViewerHooks::onEventLoggingRegisterSchemas"
+ ],
+ "UserGetDefaultOptions": [
+ "MultimediaViewerHooks::onUserGetDefaultOptions"
+ ],
+ "GetPreferences": [
+ "MultimediaViewerHooks::getPreferences"
+ ],
+ "GetBetaFeaturePreferences": [
+ "MultimediaViewerHooks::getBetaPreferences"
+ ],
+ "BeforePageDisplay": [
+ "MultimediaViewerHooks::getModulesForArticle"
+ ],
+ "CategoryPageView": [
+ "MultimediaViewerHooks::getModulesForCategory"
+ ],
+ "ResourceLoaderGetConfigVars": [
+ "MultimediaViewerHooks::resourceLoaderGetConfigVars"
+ ],
+ "MakeGlobalVariablesScript": [
+ "MultimediaViewerHooks::makeGlobalVariablesScript"
+ ],
+ "ResourceLoaderTestModules": [
+ "MultimediaViewerHooks::getTestModules"
+ ],
+ "ThumbnailBeforeProduceHTML": [
+ "MultimediaViewerHooks::thumbnailBeforeProduceHTML"
+ ]
+ },
+ "config": {
+ "MediaViewerExtensions": {
+ "jpg": "default",
+ "jpeg": "default",
+ "gif": "default",
+ "svg": "default",
+ "png": "default",
+ "tiff": "default",
+ "tif": "default"
+ },
+ "MediaViewerNetworkPerformanceSamplingFactor": false,
+ "MediaViewerDurationLoggingSamplingFactor": false,
+ "MediaViewerDurationLoggingLoggedinSamplingFactor": false,
+ "MediaViewerAttributionLoggingSamplingFactor": false,
+ "MediaViewerDimensionLoggingSamplingFactor": false,
+ "MediaViewerActionLoggingSamplingFactorMap": false,
+ "MediaViewerIsInBeta": false,
+ "MediaViewerUseThumbnailGuessing": false,
+ "MediaViewerEnableByDefault": true,
+ "MediaViewerEnableByDefaultForAnonymous": null,
+ "MediaViewerImageQueryParameter": false,
+ "MediaViewerRecordVirtualViewBeaconURI": false
+ },
+ "manifest_version": 1
+}
diff --git a/www/wiki/extensions/MultimediaViewer/gitinfo.json b/www/wiki/extensions/MultimediaViewer/gitinfo.json
new file mode 100644
index 00000000..88dd2ef0
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/gitinfo.json
@@ -0,0 +1 @@
+{"headSHA1": "1273d3e0b2189e33fed0567f589a3e7bc4ae6d4e\n", "head": "1273d3e0b2189e33fed0567f589a3e7bc4ae6d4e\n", "remoteURL": "https://gerrit.wikimedia.org/r/mediawiki/extensions/MultimediaViewer", "branch": "1273d3e0b2189e33fed0567f589a3e7bc4ae6d4e\n", "headCommitDate": "1526068630"} \ No newline at end of file
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/af.json b/www/wiki/extensions/MultimediaViewer/i18n/af.json
new file mode 100644
index 00000000..a6b160ab
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/af.json
@@ -0,0 +1,103 @@
+{
+ "@metadata": {
+ "authors": [
+ "Fwolff"
+ ]
+ },
+ "multimediaviewer-pref": "Mediakyker",
+ "multimediaviewer-optin-pref": "Aktiveer <span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About Mediakyker]</span>",
+ "multimediaviewer-file-page": "Besoek die ooreenstemmende lêerbladsy",
+ "multimediaviewer-repository-local": "Meer besonderhede",
+ "multimediaviewer-datetime-created": "Geskep: $1",
+ "multimediaviewer-datetime-uploaded": "Opgelaai: $1",
+ "multimediaviewer-credit-fallback": "Bekyk outeurinligting",
+ "multimediaviewer-multiple-authors": "{{PLURAL:$1|nog een outeur|nog $1 outeurs}}",
+ "multimediaviewer-multiple-authors-combine": "$1 en $2",
+ "multimediaviewer-metadata-error": "Kon nie die beeld se besonderhede laai nie (fout: $1)",
+ "multimediaviewer-thumbnail-error": "Jammer! Die lêer kan nie vertoon word nie",
+ "multimediaviewer-thumbnail-error-retry": "probeer weer",
+ "multimediaviewer-license-cc-pd": "Publieke domein",
+ "multimediaviewer-license-pd": "Publieke domein",
+ "multimediaviewer-license-default": "Sien lisensie",
+ "multimediaviewer-permission-title": "Detail van toestemming",
+ "multimediaviewer-permission-link": "sien voorwaardes",
+ "multimediaviewer-permission-link-hide": "versteek voorwaardes",
+ "multimediaviewer-permission-viewmore": "Wys meer",
+ "multimediaviewer-about-mmv": "Aangaande",
+ "multimediaviewer-discuss-mmv": "Bespreking",
+ "multimediaviewer-help-mmv": "Hulp",
+ "multimediaviewer-optout-mmv": "Deaktiveer Mediakyker",
+ "multimediaviewer-optin-mmv": "Aktiveer Mediakyker",
+ "multimediaviewer-optout-pending-mmv": "Deaktiveer tans Mediakyker",
+ "multimediaviewer-optin-pending-mmv": "Aktiveer tans Mediakyker",
+ "multimediaviewer-optout-help": "Mediakyker sal nie meer gebruik word om beelde te wys nie. Om dit weer te gebruik, klik op die \"{{int:multimediaviewer-view-expanded}}\"-knop langs enige beeld. Klik dan op \"{{int:multimediaviewer-optin-mmv}}\".",
+ "multimediaviewer-optin-help": "Mediakyker sal gebruik word om beelde te wys.",
+ "multimediaviewer-geolocation": "Ligging: $1",
+ "multimediaviewer-reuse-link": "Deel dié lêer of bed dit in",
+ "multimediaviewer-reuse-loading-placeholder": "Laai tans…",
+ "multimediaviewer-reuse-copy-share": "Merk en kopieer (indien ondersteun) die skakel om dié lêer te deel",
+ "multimediaviewer-reuse-copy-embed": "Merk en kopieer (indien ondersteun) die skakel om dié lêer in te bed",
+ "multimediaviewer-share-tab": "Deel",
+ "multimediaviewer-embed-tab": "Bed in",
+ "multimediaviewer-download-link": "Laai dié lêer af",
+ "multimediaviewer-download-preview-link-title": "Bekyk in blaaier",
+ "multimediaviewer-download-original-button-name": "Laai oorspronklike lêer af",
+ "multimediaviewer-download-small-button-name": "Laai klein grootte af",
+ "multimediaviewer-download-medium-button-name": "Laai mediumgrootte af",
+ "multimediaviewer-download-large-button-name": "Laai groot grootte af",
+ "multimediaviewer-link-to-page": "Skakel na die lêer se beskrywingsblad",
+ "multimediaviewer-link-to-file": "Skakel na oorspronklike lêer",
+ "multimediaviewer-share-explanation": "Kopieer en deel gerus die skakel",
+ "multimediaviewer-embed-wt": "Wikiteks",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-embed-explanation": "Gebruik dié kode om die lêer in te bed",
+ "multimediaviewer-text-embed-credit-text-bl": "Deur $1, $2, $3",
+ "multimediaviewer-text-embed-credit-text-b": "Deur $1, $2",
+ "multimediaviewer-html-embed-credit-text-bl": "Deur $1, $2, $3",
+ "multimediaviewer-html-embed-credit-text-b": "Deur $1, $2",
+ "multimediaviewer-html-embed-credit-link-text": "Skakel",
+ "multimediaviewer-embed-byline": "Deur $1",
+ "multimediaviewer-embed-license": "Gelisensieer onderhewig aan $1.",
+ "multimediaviewer-embed-via": "Via $1.",
+ "multimediaviewer-default-embed-dimensions": "Verstekgrootte vir duimnael",
+ "multimediaviewer-original-embed-dimensions": "Oorspronklike lêer $1",
+ "multimediaviewer-large-embed-dimensions": "Groot $1",
+ "multimediaviewer-medium-embed-dimensions": "Medium $1",
+ "multimediaviewer-small-embed-dimensions": "Klein $1",
+ "multimediaviewer-description-page-button-text": "Meer besonderhede oor dié lêer",
+ "multimediaviewer-description-page-popup-text": "Meer besonderhede oor dié lêer by $1",
+ "multimediaviewer-commons-subtitle": "Die vry mediastoorplek",
+ "multimediaviewer-view-expanded": "Open in Mediakyker",
+ "multimediaviewer-view-config": "Instellings",
+ "multimediaviewer-close-popup-text": "Sluit dié aansig (Esc)",
+ "multimediaviewer-fullscreen-popup-text": "Wys in volle skerm",
+ "multimediaviewer-defullscreen-popup-text": "Verlaat volle skerm",
+ "multimediaviewer-next-image-alt-text": "Wys volgende beeld",
+ "multimediaviewer-prev-image-alt-text": "Wys vorige beeld",
+ "multimediaviewer-title-popup-text": "Beskrywing",
+ "multimediaviewer-credit-popup-text": "Outeur- en broninligting",
+ "multimediaviewer-title-popup-text-more": "Sien volle beskrywing",
+ "multimediaviewer-credit-popup-text-more": "Sien volle outeur en bron",
+ "multimediaviewer-download-attribution-cta-header": "Die outeur se naam moet vermeld word",
+ "multimediaviewer-download-optional-attribution-cta-header": "Die outeur se naam kan vermeld word",
+ "multimediaviewer-download-attribution-cta": "Wys my hoe",
+ "multimediaviewer-download-attribution-copy": "Merk en kopieer (indien ondersteun) die die outeursvermelding vir dié lêer",
+ "multimediaviewer-attr-plain": "Eenvoudig",
+ "multimediaviewer-options-tooltip": "Aktiveer of deaktiveer Mediakyker",
+ "multimediaviewer-options-dialog-header": "Deaktiveer Mediakyker?",
+ "multimediaviewer-options-text-header": "Slaan hierdie aansig vir alle lêers oor.",
+ "multimediaviewer-options-text-body": "Dit kan later weer geaktiveer word op die lêer se besonderhedebladsy.",
+ "multimediaviewer-options-learn-more": "Meer inligting",
+ "multimediaviewer-option-submit-button": "Deaktiveer Mediakyker",
+ "multimediaviewer-option-cancel-button": "Kanselleer",
+ "multimediaviewer-disable-confirmation-header": "Mediakyker is gedeaktiveer",
+ "multimediaviewer-disable-confirmation-text": "Met die volgende klik op 'n duimnael op $1, sal u direk die lêerbesonderhede sien.",
+ "multimediaviewer-enable-dialog-header": "Aktiveer Mediakyker?",
+ "multimediaviewer-enable-text-header": "Aktiveer dié manier om media te kyk by verstek vir alle lêers.",
+ "multimediaviewer-enable-submit-button": "Aktiveer Mediakyker",
+ "multimediaviewer-enable-confirmation-header": "Mediakyker is geaktiveer vir alle lêers",
+ "multimediaviewer-enable-confirmation-text": "Met die volgende klik op 'n duimnael op $1 sal Mediakyker gebruik word.",
+ "multimediaviewer-enable-alert": "Mediakyker is nou gedaktiveerd",
+ "multimediaviewer-disable-info-title": "Mediakyker is gedeaktiveer",
+ "multimediaviewer-disable-info": "Dit is steeds moontlik om individuele lêers met Mediakyker te bekyk."
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/ais.json b/www/wiki/extensions/MultimediaViewer/i18n/ais.json
new file mode 100644
index 00000000..6f73fe43
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/ais.json
@@ -0,0 +1,10 @@
+{
+ "@metadata": {
+ "authors": [
+ "Benel"
+ ]
+ },
+ "multimediaviewer-geolocation": "kahicelaan: $1",
+ "multimediaviewer-reuse-loading-placeholder": "miasip henay ayza...",
+ "multimediaviewer-embed-wt": "Wikitext"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/ar.json b/www/wiki/extensions/MultimediaViewer/i18n/ar.json
new file mode 100644
index 00000000..c4e45550
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/ar.json
@@ -0,0 +1,133 @@
+{
+ "@metadata": {
+ "authors": [
+ "Claw eg",
+ "Tarawneh",
+ "مشعل الحربي",
+ "زكريا",
+ "OsamaK",
+ "محمد أحمد عبد الفتاح",
+ "Maroen1990",
+ "ديفيد"
+ ]
+ },
+ "multimediaviewer-desc": "زيادة حجم الصور المصغرة في واجهة ملء الشاشة.",
+ "multimediaviewer-pref": "عارض الوسائط",
+ "multimediaviewer-pref-desc": "حسن تجربة مشاهدة الوسائط المتعددة بهذه الأداة الجديدة، حيث تعمل على عرض الصور بحجم أكبر على الصفحات التي تحتوي صورا مصغرة. وتظهر الصور في صندوق منبثق أجمل، ويمكن أيضا عرضها بالحجم الكامل.",
+ "multimediaviewer-optin-pref": "تمكين <span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About عارض الوسائط]</span>",
+ "multimediaviewer-file-page": "الذهاب إلى الصفحة التابعة للملف",
+ "multimediaviewer-repository-local": "مزيد من التفاصيل",
+ "multimediaviewer-datetime-created": "الإنشاء: $1",
+ "multimediaviewer-datetime-uploaded": "الرفع: $1",
+ "multimediaviewer-credit-fallback": "اعرض معلومات المؤلف",
+ "multimediaviewer-multiple-authors": "{{PLURAL:$1|مؤلف واحد آخر|$1 مؤلفين آخرين}}",
+ "multimediaviewer-multiple-authors-combine": "$1 و$2",
+ "multimediaviewer-metadata-error": "خطأ: تعذر تحميل بيانات الصورة(خطا: $1)",
+ "multimediaviewer-thumbnail-error": "خطأ: تعذر تحميل الملف معطل",
+ "multimediaviewer-thumbnail-error-description": "يبدو أن هناك مشكلة فنية، يمكنك $1 أو $3 إذا استمرت، الخطأ: $2",
+ "multimediaviewer-thumbnail-error-retry": "أعد المحاولة",
+ "multimediaviewer-thumbnail-error-report": "أبلغ عن المشكلة",
+ "multimediaviewer-license-cc-pd": "ملكية عامة",
+ "multimediaviewer-license-pd": "ملكية عامة",
+ "multimediaviewer-license-default": "عرض الترخيص",
+ "multimediaviewer-permission-title": "تفاصيل الترخيص",
+ "multimediaviewer-permission-link": "راجع الشروط",
+ "multimediaviewer-permission-link-hide": "أخف الشروط",
+ "multimediaviewer-permission-viewmore": "عرض المزيد",
+ "multimediaviewer-restriction-aus-reserve": "تم تصوير هذه الصورة في محمية كومنولث الأسترالية ولا يمكن استخدامها لتحقيق مكاسب تجارية دون تصريح.",
+ "multimediaviewer-restriction-communist": "تحتوي هذه الصورة على شارات شيوعية يمكن حظرها في بعض الدول.",
+ "multimediaviewer-restriction-costume": "تعرض هذه الصورة إلباسا وقد تخضع لقيود قانونية.",
+ "multimediaviewer-restriction-currency": "تمثل هذه الصورة صورة لوحدة عملة وقد تخضع لقيود قانونية.",
+ "multimediaviewer-restriction-design": "قد يكون تصميم موضوع هذه الصورة محميا بحقوق الطبع والنشر وخاضعا لقيود قانونية.",
+ "multimediaviewer-restriction-fan-art": "هذه الصورة عمل فني للمعجبين، وقد تخضع إعادة الاستخدام لقيود قانونية.",
+ "multimediaviewer-restriction-ihl": "تحتوي هذه الصورة على رموز مقيدة بموجب القانون الدولي الإنساني.",
+ "multimediaviewer-restriction-insignia": "تحتوي هذه الصورة على شارة رسمية قد تخضع لقيود قانونية.",
+ "multimediaviewer-restriction-ita-mibac": "تستنسخ هذه الصورة ملكية تابعة للتراث الثقافي الإيطالي وتقتصر على القانون الإيطالي.",
+ "multimediaviewer-restriction-nazi": "تحتوي هذه الصورة على شارات نازية أو فاشية أخرى قد يتم حظرها في بعض الدول.",
+ "multimediaviewer-restriction-personality": "تحتوي هذه الصورة على أشخاص قد تكون لهم حقوق تقيد بشكل قانوني استخدامات معينة من الصورة بدون موافقة.",
+ "multimediaviewer-restriction-trademarked": "هذه الصورة تتضمن محتوى قد يخضع لقوانين العلامات التجارية.",
+ "multimediaviewer-restriction-default": "قد تكون هذه الصورة مقيدة بأحكام قانونية خارج قانون حقوق النشر، انظر صفحة وصف الملف للتفاصيل.",
+ "multimediaviewer-restriction-default-and-others": "يمكن تقييد هذه الصورة بمقتضى أحكام قانونية أخرى خارج قانون حقوق النشر، انظر صفحة وصف الملف للتفاصيل.",
+ "multimediaviewer-about-mmv": "حول",
+ "multimediaviewer-discuss-mmv": "نقاش",
+ "multimediaviewer-help-mmv": "مساعدة",
+ "multimediaviewer-optout-mmv": "تعطيل عارض الوسائط",
+ "multimediaviewer-optin-mmv": "مكّن عارض الوسائط",
+ "multimediaviewer-optout-pending-mmv": "يُعطّل عارض الوسائط",
+ "multimediaviewer-optin-pending-mmv": "يُفعّل عارض الوسائط",
+ "multimediaviewer-optout-help": "لن يستخدم عارض الوسائط لإظهار الصور، لتستخدمه مجددا انقر زر \"{{int:multimediaviewer-view-expanded}}\" بجانب أية صورة ثم انقر \"{{int:multimediaviewer-optin-mmv}}\".",
+ "multimediaviewer-optin-help": "سيُستخدَم عارض الوسائط لمشاهدة الصور.",
+ "multimediaviewer-geolocation": "المكان: $1",
+ "multimediaviewer-reuse-link": "شارك أو ضمن هذا الملف",
+ "multimediaviewer-reuse-loading-placeholder": "تحميل...",
+ "multimediaviewer-reuse-copy-share": "حدد وانسخ (إذا كان مدعوما) الرابط لمشاركة هذا الملف",
+ "multimediaviewer-reuse-copy-embed": "حدد وانسخ (إذا كان مدعوما) رمز تضمين هذا الملف",
+ "multimediaviewer-share-tab": "شارك",
+ "multimediaviewer-embed-tab": "ضمّن",
+ "multimediaviewer-download-link": "نزّل هذا الملف",
+ "multimediaviewer-download-preview-link-title": "اعرض في المتصفح",
+ "multimediaviewer-download-original-button-name": "نزّل الملف الأصلي",
+ "multimediaviewer-download-small-button-name": "نزّل بحجم صغير",
+ "multimediaviewer-download-medium-button-name": "نزّل بحجم متوسط",
+ "multimediaviewer-download-large-button-name": "نزّل بحجم كبير",
+ "multimediaviewer-link-to-page": "وصلة لصفحة وصف الملف",
+ "multimediaviewer-link-to-file": "وصلة للملف الأصلي",
+ "multimediaviewer-share-explanation": "انسخ الرابط وانشره",
+ "multimediaviewer-embed-wt": "نص ويكي",
+ "multimediaviewer-embed-html": "إتش تي إم إل",
+ "multimediaviewer-embed-explanation": "استخدم هذا الكود لتضمين الملف",
+ "multimediaviewer-text-embed-credit-text-bl": "بواسطة $1 و$2 و$3",
+ "multimediaviewer-text-embed-credit-text-b": "بواسطة $1 و$2",
+ "multimediaviewer-html-embed-credit-text-bl": "بواسطة $1 و$2 و$3",
+ "multimediaviewer-html-embed-credit-text-b": "بواسطة $1 و$2",
+ "multimediaviewer-html-embed-credit-link-text": "رابط",
+ "multimediaviewer-embed-byline": "صاحب العمل: $1",
+ "multimediaviewer-embed-license": "الترخيص: $1",
+ "multimediaviewer-embed-via": "المصدر: $1",
+ "multimediaviewer-default-embed-dimensions": "الحجم الافتراضي للصورة المصغرة",
+ "multimediaviewer-original-embed-dimensions": "الملف الأصلي $1",
+ "multimediaviewer-large-embed-dimensions": "كبير $1",
+ "multimediaviewer-medium-embed-dimensions": "متوسط $1",
+ "multimediaviewer-small-embed-dimensions": "صغير $1",
+ "multimediaviewer-description-page-button-text": "المزيد من التفاصيل عن هذا الملف",
+ "multimediaviewer-description-page-popup-text": "تفاصيل أكثر عن هذا الملف على $1",
+ "multimediaviewer-commons-subtitle": "مستودع الوسائط الحر",
+ "multimediaviewer-view-expanded": "افتح في عارض الوسائط",
+ "multimediaviewer-view-config": "ضبط",
+ "multimediaviewer-close-popup-text": "إغلاق هذه الأداة (Esc)",
+ "multimediaviewer-fullscreen-popup-text": "إظهار ملء كاملة",
+ "multimediaviewer-defullscreen-popup-text": "إغلاق ملء الشاشة",
+ "multimediaviewer-next-image-alt-text": "إظهار الصورة التالية",
+ "multimediaviewer-prev-image-alt-text": "عرض الصورة السابقة",
+ "multimediaviewer-title-popup-text": "الوصف",
+ "multimediaviewer-credit-popup-text": "معلومات المؤلف والمصدر",
+ "multimediaviewer-title-popup-text-more": "اعرض الوصف الكامل",
+ "multimediaviewer-credit-popup-text-more": "اعرض المؤلف والمصدر كاملين",
+ "multimediaviewer-download-attribution-cta-header": "يجب أن تنسب للمؤلف",
+ "multimediaviewer-download-optional-attribution-cta-header": "يمكنك أن تنسب للمؤلف",
+ "multimediaviewer-download-attribution-cta": "أرني كيف",
+ "multimediaviewer-download-attribution-copy": "حدد وانسخ (إذا كان مدعوما) نص الإحالة لهذا الملف",
+ "multimediaviewer-reuse-warning-deletion": "تم اقتراح حذف هذا الملف.",
+ "multimediaviewer-reuse-warning-nonfree": "لا يحتوي هذا الملف على ترخيص حر.",
+ "multimediaviewer-reuse-warning-noattribution": "لا يحتوي هذا الملف على معلومات إحالة.",
+ "multimediaviewer-reuse-warning-generic": "تحقق من [$1 تفاصيله] قبل استخدامه.",
+ "multimediaviewer-attr-plain": "عادي",
+ "multimediaviewer-options-tooltip": "فعل أو عطل عارض الوسائط",
+ "multimediaviewer-options-dialog-header": "تعطيل عارض الوسائط؟",
+ "multimediaviewer-options-text-header": "تخطي ميزة العرض هذه لجميع الملفات.",
+ "multimediaviewer-options-text-body": "يمكنك تمكينه لاحقا من خلال صفحة تفاصيل الملف.",
+ "multimediaviewer-options-learn-more": "تعرف على المزيد",
+ "multimediaviewer-option-submit-button": "عطل عارض الوسائط",
+ "multimediaviewer-option-cancel-button": "ألغ",
+ "multimediaviewer-disable-confirmation-header": "لقد عطلت عارض الوسائط",
+ "multimediaviewer-disable-confirmation-text": "في المرة التالية التي تنقر فيها على الصورة المصغرة في $1، ستقوم بعرض كل تفاصيل الملف مباشرة.",
+ "multimediaviewer-enable-dialog-header": "تفعيل عارض الوسائط؟",
+ "multimediaviewer-enable-text-header": "تمكين ميزة عرض الوسائط هذه لجميع الملفات افتراضيا.",
+ "multimediaviewer-enable-submit-button": "تفعيل عارض الوسائط",
+ "multimediaviewer-enable-confirmation-header": "لقد فعلت عارض الوسائط لكل الملفات",
+ "multimediaviewer-enable-confirmation-text": "في المرة التالية التي تنقر فيها على الصورة المصغرة في $1 ، سيتم استخدام عارض الوسائط.",
+ "multimediaviewer-enable-alert": "عارض الوسائط غير مفعل الآن",
+ "multimediaviewer-disable-info-title": "لقد عطلت عارض الوسائط",
+ "multimediaviewer-disable-info": "ما زال بإمكانك عرض الملفات المنفردة بعارض الوسائط.",
+ "multimediaviewer-errorreport-privacywarning": "يتم إرفاق تفاصيل الخطأ بالتقرير، والذي سيكون قابلا للعرض للجمهور، إذا لم تكن مرتاحا لذلك، فيمكنك تحرير التقرير أدناه وإزالة جميع البيانات التي لا تريد مشاركتها."
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/as.json b/www/wiki/extensions/MultimediaViewer/i18n/as.json
new file mode 100644
index 00000000..8a691464
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/as.json
@@ -0,0 +1,18 @@
+{
+ "@metadata": {
+ "authors": [
+ "Bishnu Saikia",
+ "Dibya Dutta"
+ ]
+ },
+ "multimediaviewer-thumbnail-error-description": "কিবা কৌশলগত সমস্যা থাকিব পাৰে। আপুনি $1 বা $3 কৰিব পাৰে। ক্ৰুটি: $2",
+ "multimediaviewer-thumbnail-error-report": "সমস্যাটো অভিযোগ কৰক",
+ "multimediaviewer-share-tab": "বিতৰণ",
+ "multimediaviewer-download-preview-link-title": "ডাউনল’ড",
+ "multimediaviewer-download-original-button-name": "প্ৰকৃত আকৃতি ডাউনল’ড কৰক",
+ "multimediaviewer-download-large-button-name": "ডাঙৰ আকৃতি ডাউনল’ড কৰক",
+ "multimediaviewer-embed-wt": "ৱিকিপাঠ্য",
+ "multimediaviewer-embed-byline": "$1 ৰ দ্বাৰা",
+ "multimediaviewer-title-popup-text": "বিৱৰণ",
+ "multimediaviewer-title-popup-text-more": "সম্পূৰ্ণ বিৱৰণ চাওক"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/ast.json b/www/wiki/extensions/MultimediaViewer/i18n/ast.json
new file mode 100644
index 00000000..ddce17a5
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/ast.json
@@ -0,0 +1,127 @@
+{
+ "@metadata": {
+ "authors": [
+ "Xuacu"
+ ]
+ },
+ "multimediaviewer-desc": "Espande les miniatures a mayor tamañu nuna interfaz a pantalla completa.",
+ "multimediaviewer-pref": "Visor de medios",
+ "multimediaviewer-pref-desc": "Ameyore la esperiencia al ver multimedia con esta nueva ferramienta. Amuesa les imaxes a mayor tamañu nes páxines que tienen miniatures. Les imaxes vense nuna guapa capa a pantalla completa, y puen vese tamién a tamañu completu.",
+ "multimediaviewer-optin-pref": "Activar el <span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About visor de multimedia]</span>",
+ "multimediaviewer-file-page": "Dir a la páxina del ficheru correspondiente",
+ "multimediaviewer-repository-local": "Más detalles",
+ "multimediaviewer-datetime-created": "Creáu: $1",
+ "multimediaviewer-datetime-uploaded": "Xubíu: $1",
+ "multimediaviewer-credit-fallback": "Ver información del autor",
+ "multimediaviewer-multiple-authors": "{{PLURAL:$1|un autor más|$1 autores más}}",
+ "multimediaviewer-multiple-authors-combine": "$1 y $2",
+ "multimediaviewer-metadata-error": "Nun pudieron cargase los datos de la imaxe (error: $1)",
+ "multimediaviewer-thumbnail-error": "Nun ye posible amosar el ficheru",
+ "multimediaviewer-thumbnail-error-description": "Paez qu'hai un problema téunicu. Pues $1 o $3 si siguiera produciéndose. Error: $2",
+ "multimediaviewer-thumbnail-error-retry": "reintentar",
+ "multimediaviewer-thumbnail-error-report": "informar del problema",
+ "multimediaviewer-license-cc-pd": "Dominiu públicu",
+ "multimediaviewer-license-pd": "Dominiu públicu",
+ "multimediaviewer-license-default": "Ver la llicencia",
+ "multimediaviewer-permission-title": "Detalles del permisu",
+ "multimediaviewer-permission-link": "ver los términos",
+ "multimediaviewer-permission-link-hide": "anubrir condiciones",
+ "multimediaviewer-permission-viewmore": "Ver más",
+ "multimediaviewer-restriction-2257": "Esta imaxe tien conteníu sexual esplícitu que podría tar suxetu al ''Child Protection and Obscenity Enforcement Act'' nos Estaos Xunios d'América.",
+ "multimediaviewer-restriction-aus-reserve": "Esta imaxe tomóse n'una reserva de la Commonwealth Australiana y nun pué usase pa tener ganancies comerciales sin permisu.",
+ "multimediaviewer-restriction-communist": "Esta imaxe contien insinies comunistes que podríen tar torgaes en determinaos paises.",
+ "multimediaviewer-restriction-costume": "Esta imaxe amuesa vistidures y podría tar suxeta a torgues llegales.",
+ "multimediaviewer-restriction-currency": "Esta imaxe amuesa una imaxe d'una unidá monetaria y podría tar suxeta a torgues llegales.",
+ "multimediaviewer-restriction-design": "El diseñu del tema d'esta imaxe pué tener drechos d'autor y tar suxetu a torgues llegales.",
+ "multimediaviewer-restriction-fan-art": "Esta imaxe ye un trabayu artísticu d'un fan, y re-usala podría tar suxeto a torgues llegales.",
+ "multimediaviewer-restriction-ihl": "Esta imaxe contien símbolos torgaos pol Drechu Humanitariu Internacional.",
+ "multimediaviewer-restriction-insignia": "Esta imaxe amuesa insinies oficiales y podría tar suxeta a torgues llegales.",
+ "multimediaviewer-restriction-ita-mibac": "Esta imaxe reproduz una propiedá que pertenez al patrimoniu cultural italianu y ta torgada pola llei italiana.",
+ "multimediaviewer-restriction-nazi": "Esta imaxe contien insinies nazis o fascistes que podríen tar torgaes en determinaos paises.",
+ "multimediaviewer-restriction-personality": "Esta imaxe contien persones que puen tener drechos que torguen llegalmente ciertes re-utilizaciones de la imaxe sin el so consentimientu.",
+ "multimediaviewer-restriction-trademarked": "Esta imaxe tien conteníu que pué tar suxetu a les lleis de marques rexistraes.",
+ "multimediaviewer-restriction-default": "Esta imaxe pué tar torgada por otres disposiciones llegales distintes de la llei de propiedá intelectual. Mira la páxina de descripción del ficheru pa más detalles.",
+ "multimediaviewer-restriction-default-and-others": "Esta imaxe pué tar inda más torgada por otres disposiciones llegales distintes de la llei de propiedá intelectual. Mira la páxina de descripción del ficheru pa más detalles.",
+ "multimediaviewer-about-mmv": "Tocante a",
+ "multimediaviewer-discuss-mmv": "Alderique",
+ "multimediaviewer-help-mmv": "Ayuda",
+ "multimediaviewer-optout-mmv": "Desactivar el visor de multimedia",
+ "multimediaviewer-optin-mmv": "Activar el visor de multimedia",
+ "multimediaviewer-optout-pending-mmv": "Desactivando el visor de multimedia",
+ "multimediaviewer-optin-pending-mmv": "Activando el visor de multimedia",
+ "multimediaviewer-optout-help": "El Visor de multimedia yá nun s'usará pa ver les imaxes. Pa volver a usalu, fai clic nel botón «{{int:multimediaviewer-view-expanded}}» cabo cada imaxe. Darréu fai clic en «{{int:multimediaviewer-optin-mmv}}».",
+ "multimediaviewer-optin-help": "Agora les imaxes abriránse col visor de multimedia.",
+ "multimediaviewer-geolocation": "Allugamientu: $1",
+ "multimediaviewer-reuse-link": "Compartir o incrustar esti ficheru",
+ "multimediaviewer-reuse-loading-placeholder": "Cargando...",
+ "multimediaviewer-reuse-copy-share": "Seleicionar y copiar (si hai encontu) l'enllaz pa compartir esti ficheru",
+ "multimediaviewer-reuse-copy-embed": "Seleicionar y copiar (si hai encontu) el códigu pa embrivir esti ficheru",
+ "multimediaviewer-share-tab": "Compartir",
+ "multimediaviewer-embed-tab": "Incrustar",
+ "multimediaviewer-download-link": "Descargar esti ficheru",
+ "multimediaviewer-download-preview-link-title": "Ver nel navegador",
+ "multimediaviewer-download-original-button-name": "Descargar el ficheru orixinal",
+ "multimediaviewer-download-small-button-name": "Descargar el tamañu pequeñu",
+ "multimediaviewer-download-medium-button-name": "Descargar el tamañu medianu",
+ "multimediaviewer-download-large-button-name": "Descargar el tamañu grande",
+ "multimediaviewer-link-to-page": "Enllaz a la páxina de descripción del ficheru",
+ "multimediaviewer-link-to-file": "Enllaz al ficheru orixinal",
+ "multimediaviewer-share-explanation": "Copiar y compartir llibremente l'enllaz",
+ "multimediaviewer-embed-wt": "Testu wiki",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-embed-explanation": "Usar esti códigu pa incrustar el ficheru",
+ "multimediaviewer-text-embed-credit-text-bl": "Por $1, $2, $3",
+ "multimediaviewer-text-embed-credit-text-b": "Por $1, $2",
+ "multimediaviewer-html-embed-credit-text-bl": "Por $1, $2, $3",
+ "multimediaviewer-html-embed-credit-text-b": "Por $1, $2",
+ "multimediaviewer-html-embed-credit-link-text": "Enllaz",
+ "multimediaviewer-embed-byline": "Por $1",
+ "multimediaviewer-embed-license": "Baxo la llicencia $1.",
+ "multimediaviewer-embed-via": "Vía $1.",
+ "multimediaviewer-default-embed-dimensions": "Tamañu de miniatura predetermináu",
+ "multimediaviewer-original-embed-dimensions": "Ficheru orixinal $1",
+ "multimediaviewer-large-embed-dimensions": "Grande $1",
+ "multimediaviewer-medium-embed-dimensions": "Medianu $1",
+ "multimediaviewer-small-embed-dimensions": "Pequeñu $1",
+ "multimediaviewer-description-page-button-text": "Más detalles d'esti ficheru",
+ "multimediaviewer-description-page-popup-text": "Más detalles d'esti ficheru en $1",
+ "multimediaviewer-commons-subtitle": "El depósitu de multimedia llibre",
+ "multimediaviewer-view-expanded": "Abrir nel visor de multimedia",
+ "multimediaviewer-view-config": "Configuración",
+ "multimediaviewer-close-popup-text": "Zarrar esta ferramienta (Esc)",
+ "multimediaviewer-fullscreen-popup-text": "Ver a pantalla completa",
+ "multimediaviewer-defullscreen-popup-text": "Colar de pantalla completa",
+ "multimediaviewer-next-image-alt-text": "Amosar la imaxe siguiente",
+ "multimediaviewer-prev-image-alt-text": "Amosar la imaxe anterior",
+ "multimediaviewer-title-popup-text": "Descripción",
+ "multimediaviewer-credit-popup-text": "Información del autor y la fonte",
+ "multimediaviewer-title-popup-text-more": "Ver la descripción completa",
+ "multimediaviewer-credit-popup-text-more": "Ver completos l'autor y la fonte",
+ "multimediaviewer-download-attribution-cta-header": "Ye necesario da-y reconocimientu al autor",
+ "multimediaviewer-download-optional-attribution-cta-header": "Puedes dar reconocimientu al autor",
+ "multimediaviewer-download-attribution-cta": "Amosame cómo",
+ "multimediaviewer-download-attribution-copy": "Seleicionar y copiar (si hai encontu) el testu d'atribución d'esti ficheru",
+ "multimediaviewer-reuse-warning-deletion": "Ta considerándose'l borráu d'esti ficheru.",
+ "multimediaviewer-reuse-warning-nonfree": "Esti ficheru nun tien una llicencia llibre.",
+ "multimediaviewer-reuse-warning-noattribution": "Esti ficheru nun tien información d'atribución.",
+ "multimediaviewer-reuse-warning-generic": "Comprueba [$1 los detalles] antes d'utilizalu.",
+ "multimediaviewer-attr-plain": "Testu planu",
+ "multimediaviewer-options-tooltip": "Activar o desactivar el Visor de multimedia",
+ "multimediaviewer-options-dialog-header": "¿Desactivar el visor de multimedia?",
+ "multimediaviewer-options-text-header": "Saltar esta función de visión pa tolos ficheros.",
+ "multimediaviewer-options-text-body": "Pues activalla más sero na páxina de detalles del ficheru.",
+ "multimediaviewer-options-learn-more": "Más información",
+ "multimediaviewer-option-submit-button": "Desactivar el visor de multimedia",
+ "multimediaviewer-option-cancel-button": "Encaboxar",
+ "multimediaviewer-disable-confirmation-header": "Desactivasti el visor de multimedia",
+ "multimediaviewer-disable-confirmation-text": "La siguiente vez que faigas clic n'una miniatura en $1, verás direutamente los detalles del ficheru.",
+ "multimediaviewer-enable-dialog-header": "¿Activar el visor de multimedia?",
+ "multimediaviewer-enable-text-header": "Activar esta función de visión pa tolos ficheros de mou predetermináu.",
+ "multimediaviewer-enable-submit-button": "Activar el visor de multimedia",
+ "multimediaviewer-enable-confirmation-header": "Activasti el visor de multimedia pa tolos ficheros",
+ "multimediaviewer-enable-confirmation-text": "La siguiente vez que faigas clic n'una miniatura en $1, usaráse'l Visor de multimedia.",
+ "multimediaviewer-enable-alert": "El Visor de multimedia agora ta desactiváu",
+ "multimediaviewer-disable-info-title": "Desactivasti el visor de multimedia",
+ "multimediaviewer-disable-info": "Inda pues ver los ficheros individualmente col Visor de multimedia.",
+ "multimediaviewer-errorreport-privacywarning": "Los detalles del error axúntense al informe, que va ser visible públicamente. Si nun tas cómodu con eso, pues editar l'informe más abaxo y desaniciar tolos datos que nun quixeras compartir."
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/atj.json b/www/wiki/extensions/MultimediaViewer/i18n/atj.json
new file mode 100644
index 00000000..2f394562
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/atj.json
@@ -0,0 +1,9 @@
+{
+ "@metadata": {
+ "authors": [
+ "Benoit Rochon"
+ ]
+ },
+ "multimediaviewer-about-mmv": "Enko nehe",
+ "multimediaviewer-title-popup-text": "E witcikemakak"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/av.json b/www/wiki/extensions/MultimediaViewer/i18n/av.json
new file mode 100644
index 00000000..171ccdef
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/av.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Аль-Гимравий"
+ ]
+ },
+ "multimediaviewer-discuss-mmv": "Гьоркьоб лъей"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/az.json b/www/wiki/extensions/MultimediaViewer/i18n/az.json
new file mode 100644
index 00000000..05cbaeac
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/az.json
@@ -0,0 +1,10 @@
+{
+ "@metadata": {
+ "authors": [
+ "Serkanland",
+ "Wertuose"
+ ]
+ },
+ "multimediaviewer-help-mmv": "Kömək",
+ "multimediaviewer-optin-mmv": "Media nümayişetdiricisini işə sal"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/bcl.json b/www/wiki/extensions/MultimediaViewer/i18n/bcl.json
new file mode 100644
index 00000000..6f65ee9e
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/bcl.json
@@ -0,0 +1,10 @@
+{
+ "@metadata": {
+ "authors": [
+ "Geopoet"
+ ]
+ },
+ "multimediaviewer-permission-title": "Mga detalye kan Lisensiya",
+ "multimediaviewer-permission-link": "Hilngon an mga termino",
+ "multimediaviewer-optin-help": "Parapahiling sa Media gagamiton sa pagpapahiling nin mga imahe."
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/be-tarask.json b/www/wiki/extensions/MultimediaViewer/i18n/be-tarask.json
new file mode 100644
index 00000000..d2c31bb1
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/be-tarask.json
@@ -0,0 +1,46 @@
+{
+ "@metadata": {
+ "authors": [
+ "Red Winged Duck"
+ ]
+ },
+ "multimediaviewer-optin-pref": "Уключыць <span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About мэдыяпраглядальнік]</span>",
+ "multimediaviewer-repository-local": "Падрабязьней",
+ "multimediaviewer-datetime-created": "Створаны: $1",
+ "multimediaviewer-permission-link": "паказаць умовы",
+ "multimediaviewer-about-mmv": "Апісаньне",
+ "multimediaviewer-discuss-mmv": "Абмеркаваньне",
+ "multimediaviewer-help-mmv": "Даведка",
+ "multimediaviewer-optin-mmv": "Уключыць мэдыяпраглядальнік",
+ "multimediaviewer-geolocation": "Месцазнаходжаньне: $1",
+ "multimediaviewer-reuse-link": "Падзяліцца або выкарыстаць гэты файл",
+ "multimediaviewer-share-tab": "Падзяліцца",
+ "multimediaviewer-embed-tab": "Выкарыстаць",
+ "multimediaviewer-download-link": "Спампаваць гэты файл",
+ "multimediaviewer-share-explanation": "Скапіюйце і свабодна дзяліцеся спасылкай",
+ "multimediaviewer-embed-wt": "Вікітэкст",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-embed-explanation": "Выкарыстоўвайце гэты код для ўстаўкі файла",
+ "multimediaviewer-default-embed-dimensions": "Памер мініятуры па змоўчаньні",
+ "multimediaviewer-original-embed-dimensions": "Першапачатковы файл $1",
+ "multimediaviewer-large-embed-dimensions": "Вялікі памер $1",
+ "multimediaviewer-medium-embed-dimensions": "Сярэдні памер $1",
+ "multimediaviewer-small-embed-dimensions": "Малы памер $1",
+ "multimediaviewer-view-expanded": "Адкрыць у мэдыяпраглядальніку",
+ "multimediaviewer-close-popup-text": "Закрыць гэты інструмэнт (Esc)",
+ "multimediaviewer-fullscreen-popup-text": "Паказаць на ўвесь экран",
+ "multimediaviewer-options-tooltip": "Уключыць або выключыць мэдыяпраглядальнік",
+ "multimediaviewer-options-dialog-header": "Адключыць мэдыяпраглядальнік?",
+ "multimediaviewer-options-text-header": "Адключыць гэтую функцыю прагляду для ўсіх файлаў.",
+ "multimediaviewer-options-text-body": "Вы можаце потым уключыць яго на старонцы зьвестак пра файл.",
+ "multimediaviewer-options-learn-more": "Даведацца болей",
+ "multimediaviewer-option-submit-button": "Адключыць мэдыяпраглядальнік",
+ "multimediaviewer-option-cancel-button": "Адмяніць",
+ "multimediaviewer-disable-confirmation-header": "Вы адключылі мэдыяпраглядальнік",
+ "multimediaviewer-disable-confirmation-text": "Наступны раз, калі вы націсьніце на мініятуру на сайце $1, вы наўпрост пабачыце ўсе зьвесткі пра файл.",
+ "multimediaviewer-enable-text-header": "Уключыць гэтую функцыю мэдыяпрагляду па змоўчаньні для ўсіх файлаў.",
+ "multimediaviewer-enable-submit-button": "Уключыць мэдыяпраглядальнік",
+ "multimediaviewer-enable-confirmation-header": "Вы ўключылі мэдыяпраглядальнік для ўсіх файлаў",
+ "multimediaviewer-enable-confirmation-text": "Наступны раз, калі вы націсьніце на мініятуру на сайце $1, будзе выкарыстаны мэдыяпраглядальнік.",
+ "multimediaviewer-enable-alert": "Мэдыяпраглядальнік цяпер адключаны"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/be.json b/www/wiki/extensions/MultimediaViewer/i18n/be.json
new file mode 100644
index 00000000..0f6f23a8
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/be.json
@@ -0,0 +1,9 @@
+{
+ "@metadata": {
+ "authors": [
+ "Artsiom91"
+ ]
+ },
+ "multimediaviewer-optin-pref": "Уключыць <span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About Праглядчык медыяфайлаў]</span>",
+ "multimediaviewer-optin-mmv": "Уключыць «Праглядчык медыяфайлаў»"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/bg.json b/www/wiki/extensions/MultimediaViewer/i18n/bg.json
new file mode 100644
index 00000000..55ecd376
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/bg.json
@@ -0,0 +1,21 @@
+{
+ "@metadata": {
+ "authors": [
+ "Mitzev",
+ "DCLXVI",
+ "Vodnokon4e",
+ "StanProg",
+ "ShockD"
+ ]
+ },
+ "multimediaviewer-optin-pref": "Включване на <span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About Media Viewer]</span>",
+ "multimediaviewer-datetime-created": "Създадено: $1",
+ "multimediaviewer-permission-title": "Информация за лиценза",
+ "multimediaviewer-permission-link": "вижте условията",
+ "multimediaviewer-permission-viewmore": "Вижте още",
+ "multimediaviewer-optin-mmv": "Включване на Media Viewer",
+ "multimediaviewer-share-tab": "Споделяне",
+ "multimediaviewer-title-popup-text": "Описание",
+ "multimediaviewer-title-popup-text-more": "Разгледай пълно описание",
+ "multimediaviewer-credit-popup-text-more": "Покажи пълна информация за автора и източника"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/bgn.json b/www/wiki/extensions/MultimediaViewer/i18n/bgn.json
new file mode 100644
index 00000000..945328e7
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/bgn.json
@@ -0,0 +1,9 @@
+{
+ "@metadata": {
+ "authors": [
+ "Ibrahim khashrowdi"
+ ]
+ },
+ "multimediaviewer-about-mmv": "بِه باره‌ئا",
+ "multimediaviewer-discuss-mmv": "حبر/گپ"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/bn.json b/www/wiki/extensions/MultimediaViewer/i18n/bn.json
new file mode 100644
index 00000000..68978576
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/bn.json
@@ -0,0 +1,130 @@
+{
+ "@metadata": {
+ "authors": [
+ "Aftab1995",
+ "Tauhid16",
+ "Aftabuzzaman",
+ "Macofe"
+ ]
+ },
+ "multimediaviewer-desc": "একটি পূর্ণস্ক্রিন ইন্টারফেসের মধ্যে একটি বড় মাপের থাম্বনেল প্রসারিত করে।",
+ "multimediaviewer-pref": "মিডিয়া ভিউয়ার",
+ "multimediaviewer-pref-desc": "এই নতুন সরঞ্জামটি দিয়ে মাল্টিমিডিয়া দেখার নতুন অভিজ্ঞতা নিন। এটা থাম্বনেল আছে এমন পাতায় বড় মাপের চিত্র প্রদর্শন করে। চিত্র পর্দা জুড়ে প্রদর্শন ইন্টারফেস বাক্সে প্রদর্শিত হয়, এছাড়াও পূর্ণ মাপ দেখা যাবে।",
+ "multimediaviewer-optin-pref": "<span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About মিডিয়া ভিউয়ার]</span> সক্রিয় করো",
+ "multimediaviewer-file-page": "সংশ্লিষ্ট ফাইল পৃষ্ঠাতে যান",
+ "multimediaviewer-repository-local": "আরো বিস্তারিত",
+ "multimediaviewer-datetime-created": "তৈরী হয়েছে: $1",
+ "multimediaviewer-datetime-uploaded": "আপলোড হয়েছে: $1",
+ "multimediaviewer-credit-fallback": "প্রণেতার তথ্য দেখুন",
+ "multimediaviewer-multiple-authors": "{{PLURAL:$1|আরও একজন প্রণেতা|আরও $1 জন প্রণেতা}}",
+ "multimediaviewer-multiple-authors-combine": "$1 এবং $2",
+ "multimediaviewer-metadata-error": "চিত্রের বিস্তারিত লোড করা যায়নি (ত্রুটি: $1)",
+ "multimediaviewer-thumbnail-error": "দুঃখিত, ফাইলটি প্রদর্শন করা যাবে না",
+ "multimediaviewer-thumbnail-error-description": "এখানে একটি প্রযুক্তিগত সমস্যা আছে বলে মনে হচ্ছে। যদি এটি চলতেই থাকে তাহলে আপনি $1 করতে বা $3 পারেন। ত্রুটি: $2",
+ "multimediaviewer-thumbnail-error-retry": "পুনঃচেষ্টা",
+ "multimediaviewer-thumbnail-error-report": "সমস্যা প্রতিবেদন করতে",
+ "multimediaviewer-license-cc-by-1.0": "সিসি বাই ১.০",
+ "multimediaviewer-license-cc-sa-1.0": "সিসি এসএ ১.০",
+ "multimediaviewer-license-cc-by-sa-1.0": "সিসি বাই-এসএ ১.০",
+ "multimediaviewer-license-cc-by-2.0": "সিসি বাই ২.০",
+ "multimediaviewer-license-cc-by-sa-2.0": "সিসি বাই-এস ২.০",
+ "multimediaviewer-license-cc-by-2.1": "সিসি বাই ২.১",
+ "multimediaviewer-license-cc-by-sa-2.1": "সিসি বাই-এসএ ২.১",
+ "multimediaviewer-license-cc-by-2.5": "সিসি বাই ২.৫",
+ "multimediaviewer-license-cc-by-sa-2.5": "সিসি বাই-এসএ ২.৫",
+ "multimediaviewer-license-cc-by-3.0": "সিসি বাই ৩.০",
+ "multimediaviewer-license-cc-by-sa-3.0": "সিসি বাই-এসএ ৩.০",
+ "multimediaviewer-license-cc-by-4.0": "সিসি বাই ৪.০",
+ "multimediaviewer-license-cc-by-sa-4.0": "সিসি বাই-এসএ ৪.০",
+ "multimediaviewer-license-cc-pd": "পাবলিক ডোমেইন",
+ "multimediaviewer-license-cc-zero": "সিসি ০",
+ "multimediaviewer-license-pd": "পাবলিক ডোমেইন",
+ "multimediaviewer-license-default": "লাইসেন্স দেখুন",
+ "multimediaviewer-permission-title": "অনুমতির বিবরণ",
+ "multimediaviewer-permission-link": "শর্তাবলী দেখুন",
+ "multimediaviewer-permission-link-hide": "শর্তাবলী আড়াল করুন",
+ "multimediaviewer-permission-viewmore": "আরও দেখুন",
+ "multimediaviewer-restriction-ihl": "এই ছবিতে আন্তর্জাতিক মানবিক আইন কর্তৃক সীমাবদ্ধ চিহ্ন রয়েছে।",
+ "multimediaviewer-restriction-nazi": "এই চিত্রে নাৎসি বা অন্যান্য ফ্যাসিবাদী পরিচয়চিহ্ন রয়েছে যা কিছু দেশে নিষিদ্ধ হতে পারে।",
+ "multimediaviewer-restriction-trademarked": "এই চিত্রে সামগ্রী রয়েছে যা ট্রেডমার্ক সংক্রান্ত আইনের সঙ্গে সামঞ্জস্যপূর্ণ হতে পারে।",
+ "multimediaviewer-about-mmv": "সম্পর্কে",
+ "multimediaviewer-discuss-mmv": "আলোচনা",
+ "multimediaviewer-help-mmv": "সাহায্য",
+ "multimediaviewer-optout-mmv": "মিডিয়া ভিউয়ার নিষ্ক্রিয় করো",
+ "multimediaviewer-optin-mmv": "মিডিয়া ভিউয়ার সক্রিয় করো",
+ "multimediaviewer-optout-pending-mmv": "মিডিয়া ভিউয়ার নিষ্ক্রিয় করো",
+ "multimediaviewer-optin-pending-mmv": "মিডিয়া ভিউয়ার সক্রিয় করা হচ্ছে",
+ "multimediaviewer-optout-help": "মিডিয়া ভিউয়ার চিত্রগুলি দেখানোর জন্য আর ব্যবহার করা হবে না। এটি আবার ব্যবহার করতে, যে কোন চিত্রের নিচে \"{{int:multimediaviewer-view-expanded}}\" বোতামে ক্লিক করুন। এরপর \"{{int:multimediaviewer-optin-mmv}}\"তে ক্লিক করুন।",
+ "multimediaviewer-optin-help": "মিডিয়া ভিউয়ার চিত্রগুলি দেখানোর জন্য ব্যবহার করা হবে।",
+ "multimediaviewer-geoloc-north": "উ",
+ "multimediaviewer-geoloc-east": "পূ",
+ "multimediaviewer-geoloc-south": "দ",
+ "multimediaviewer-geoloc-west": "প",
+ "multimediaviewer-geolocation": "অবস্থান: $1",
+ "multimediaviewer-reuse-link": "এই ফাইলটি শেয়ার বা এম্বেড করুন",
+ "multimediaviewer-reuse-loading-placeholder": "লোড হচ্ছে...",
+ "multimediaviewer-share-tab": "শেয়ার",
+ "multimediaviewer-embed-tab": "এম্বেড",
+ "multimediaviewer-download-link": "এই ফাইলটি ডাউনলোড করুন",
+ "multimediaviewer-download-preview-link-title": "ব্রাউজারে দেখুন",
+ "multimediaviewer-download-original-button-name": "মূল ফাইলটি ডাউনলোড করুন",
+ "multimediaviewer-download-small-button-name": "ছোট আকার ডাউনলোড করুন",
+ "multimediaviewer-download-medium-button-name": "মাঝারি আকার ডাউনলোড করুন",
+ "multimediaviewer-download-large-button-name": "বড় আকার ডাউনলোড করুন",
+ "multimediaviewer-link-to-page": "বিবরণ পাতায় ফাইল লিঙ্ক করুন",
+ "multimediaviewer-link-to-file": "মূল ফাইলে লিঙ্ক করুন",
+ "multimediaviewer-share-explanation": "অনুলিপি করুন এবং অবাধে লিঙ্ক শেয়ার করুন",
+ "multimediaviewer-embed-wt": "উইকিটেক্সট",
+ "multimediaviewer-embed-html": "এইচটিএমএল",
+ "multimediaviewer-embed-explanation": "ফাইল এম্বেড করার জন্য এই কোড ব্যবহার করুন",
+ "multimediaviewer-text-embed-credit-text-bl": "$1 কর্তৃক, $2, $3",
+ "multimediaviewer-text-embed-credit-text-b": "$1 কর্তৃক, $2",
+ "multimediaviewer-html-embed-credit-text-bl": "$1 কর্তৃক, $2, $3",
+ "multimediaviewer-html-embed-credit-text-b": "$1 কর্তৃক, $2",
+ "multimediaviewer-html-embed-credit-link-text": "সংযোগ",
+ "multimediaviewer-embed-byline": "$1 কর্তৃক",
+ "multimediaviewer-embed-license": "$1-এর অধীনে লাইসেন্সকৃত।",
+ "multimediaviewer-embed-via": "$1 হয়ে।",
+ "multimediaviewer-default-embed-dimensions": "ডিফল্ট থাম্বনেইল আকার",
+ "multimediaviewer-original-embed-dimensions": "মূল ফাইল $1",
+ "multimediaviewer-large-embed-dimensions": "বড় $1",
+ "multimediaviewer-medium-embed-dimensions": "মাঝারি $1",
+ "multimediaviewer-small-embed-dimensions": "ছোট $1",
+ "multimediaviewer-description-page-button-text": "এই ফাইল সম্পর্কে আরও বিস্তারিত",
+ "multimediaviewer-description-page-popup-text": "$1-এ এই ফাইল সম্পর্কে আরো বিস্তারিত",
+ "multimediaviewer-commons-subtitle": "মুক্ত মিডিয়া ভাণ্ডার",
+ "multimediaviewer-view-expanded": "মিডিয়া ভিউয়ারে খুলুন",
+ "multimediaviewer-view-config": "কনফিগারেশন",
+ "multimediaviewer-close-popup-text": "এই সরঞ্জামটি বন্ধ করুন (Esc)",
+ "multimediaviewer-fullscreen-popup-text": "পূর্ণ স্ক্রিনে দেখান",
+ "multimediaviewer-defullscreen-popup-text": "পূর্ণ স্ক্রিন প্রস্থান করুন",
+ "multimediaviewer-next-image-alt-text": "পরবর্তী চিত্র দেখান",
+ "multimediaviewer-prev-image-alt-text": "পূর্ববর্তী চিত্র দেখান",
+ "multimediaviewer-title-popup-text": "বিবরণ",
+ "multimediaviewer-credit-popup-text": "প্রণেতা এবং উৎসের তথ্য",
+ "multimediaviewer-title-popup-text-more": "সম্পূর্ণ বিবরণ দেখুন",
+ "multimediaviewer-credit-popup-text-more": "পূর্ণ প্রণেতা এবং উৎস দেখুন",
+ "multimediaviewer-download-attribution-cta-header": "আপনার প্রণেতাকে আরোপ করা প্রয়োজন",
+ "multimediaviewer-download-optional-attribution-cta-header": "আপনি প্রণেতাকে আরোপ করতে পারেন",
+ "multimediaviewer-download-attribution-cta": "কিভাবে আমাকে দেখান",
+ "multimediaviewer-reuse-warning-nonfree": "এই ফাইলটিতে একটি উন্মুক্ত লাইসেন্স নেই।",
+ "multimediaviewer-reuse-warning-generic": "এটি ব্যবহার করার আগে [$1 এর বিস্তারিত] পরীক্ষা করুন।",
+ "multimediaviewer-attr-plain": "সাধারণ",
+ "multimediaviewer-options-tooltip": "মিডিয়া ভিউয়ার সক্রিয় অথবা নিষ্ক্রিয় করুন",
+ "multimediaviewer-options-dialog-header": "মিডিয়া ভিউয়ার নিষ্ক্রিয় করবেন?",
+ "multimediaviewer-options-text-header": "সব ফাইলের জন্য এই দেখার বৈশিষ্ট্য এড়িয়ে যান।",
+ "multimediaviewer-options-text-body": "আপনি পরে ফাইলের বিবরণ পাতার মাধ্যমে এটি সক্রিয় করতে পারবেন।",
+ "multimediaviewer-options-learn-more": "আরও জানুন",
+ "multimediaviewer-option-submit-button": "মিডিয়া ভিউয়ার নিষ্ক্রিয় করো",
+ "multimediaviewer-option-cancel-button": "বাতিল",
+ "multimediaviewer-disable-confirmation-header": "আপনি মিডিয়া ভিউয়ার নিষ্ক্রিয় করেছেন",
+ "multimediaviewer-disable-confirmation-text": "পরবর্তী সময় আপনি যখন $1-এর একটি থাম্বনেইলে ক্লিক করবেন, আপনি সরাসরি ফাইলের সব বিবরণ দেখতে পাবেন।",
+ "multimediaviewer-enable-dialog-header": "মিডিয়া ভিউয়ার সক্রিয় করবেন?",
+ "multimediaviewer-enable-text-header": "ডিফল্ট হিসাবে সব ফাইলের জন্য এই মিডিয়া দেখার বৈশিষ্ট্য সক্রিয় করুন।",
+ "multimediaviewer-enable-submit-button": "মিডিয়া ভিউয়ার সক্রিয় করো",
+ "multimediaviewer-enable-confirmation-header": "আপনি সকল ফাইলের জন্য মিডিয়া ভিউয়ার সক্রিয় করেছেন",
+ "multimediaviewer-enable-confirmation-text": "পরবর্তী সময় আপনি যখন $1-এর একটি থাম্বনেইলে ক্লিক করবেন, তখন মিডিয়া ভিউয়ার ব্যবহার করা হবে।",
+ "multimediaviewer-enable-alert": "মিডিয়া ভিউয়ার এখন নিষ্ক্রিয়",
+ "multimediaviewer-disable-info-title": "আপনি মিডিয়া ভিউয়ার নিষ্ক্রিয় করেছেন",
+ "multimediaviewer-disable-info": "আপনি এখনও মিডিয়া ভিউয়ার দিয়ে পৃথক ফাইল দেখতে পারবেন।"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/br.json b/www/wiki/extensions/MultimediaViewer/i18n/br.json
new file mode 100644
index 00000000..72db21b0
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/br.json
@@ -0,0 +1,44 @@
+{
+ "@metadata": {
+ "authors": [
+ "Fulup",
+ "Y-M D",
+ "Fohanno"
+ ]
+ },
+ "multimediaviewer-repository-local": "Gouzout hiroc'h diwar-benn ar restr-mañ",
+ "multimediaviewer-datetime-created": "Krouet d'an $1",
+ "multimediaviewer-datetime-uploaded": "Enporzhiet d'an $1",
+ "multimediaviewer-multiple-authors-combine": "$1 ha $2",
+ "multimediaviewer-license-cc-pd": "Domani foran",
+ "multimediaviewer-license-pd": "Domani foran",
+ "multimediaviewer-license-default": "Gwelet an aotre-implijout",
+ "multimediaviewer-permission-title": "Munudoù an aotre-implijout",
+ "multimediaviewer-permission-viewmore": "Gwelet muioc'h",
+ "multimediaviewer-help-mmv": "Skoazell",
+ "multimediaviewer-geolocation": "Lec'hiadur : $1",
+ "multimediaviewer-reuse-link": "Implijout ar restr-mañ",
+ "multimediaviewer-reuse-loading-placeholder": "O kargañ...",
+ "multimediaviewer-share-tab": "Rannañ",
+ "multimediaviewer-embed-tab": "Enframmañ",
+ "multimediaviewer-download-link": "Pellgargañ",
+ "multimediaviewer-download-preview-link-title": "Gwelet er merdeer",
+ "multimediaviewer-download-original-button-name": "Pellgargañ ar restr orin",
+ "multimediaviewer-link-to-file": "Liamm war-zu ar restr orin",
+ "multimediaviewer-embed-wt": "Wikitestenn",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-embed-byline": "Gant $1",
+ "multimediaviewer-embed-license": "Dindan an aotre-implijout $1.",
+ "multimediaviewer-embed-via": "Eus $1.",
+ "multimediaviewer-original-embed-dimensions": "Ment orin $1",
+ "multimediaviewer-small-embed-dimensions": "Bihan $1",
+ "multimediaviewer-description-page-button-text": "Muioc'h a ditouroù diwar-benn ar restr-mañ",
+ "multimediaviewer-description-page-popup-text": "Muioc'h a ditouroù diwar-benn ar restr-mañ war $1",
+ "multimediaviewer-view-expanded": "Diskweladur astennet",
+ "multimediaviewer-fullscreen-popup-text": "Diskouez er skramm leun",
+ "multimediaviewer-defullscreen-popup-text": "Kuitaat ar skramm leun",
+ "multimediaviewer-title-popup-text": "Anv ar restr",
+ "multimediaviewer-title-popup-text-more": "Klikañ da welet anv klok ar restr",
+ "multimediaviewer-download-attribution-cta": "Diskouez din penaos ober",
+ "multimediaviewer-attr-plain": "Plaen"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/bs.json b/www/wiki/extensions/MultimediaViewer/i18n/bs.json
new file mode 100644
index 00000000..08d751f4
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/bs.json
@@ -0,0 +1,80 @@
+{
+ "@metadata": {
+ "authors": [
+ "DzWiki",
+ "Palapa",
+ "Srdjan m",
+ "Obsuser"
+ ]
+ },
+ "multimediaviewer-pref": "Preglednik medijskih datoteka",
+ "multimediaviewer-optin-pref": "Uključi <span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About Medijski preglednik]</span>",
+ "multimediaviewer-file-page": "Idi na odgovarajuću stranicu datoteke",
+ "multimediaviewer-repository-local": "Više detalja",
+ "multimediaviewer-datetime-created": "Napravljeno: $1",
+ "multimediaviewer-datetime-uploaded": "Postavljeno: $1",
+ "multimediaviewer-credit-fallback": "Prikaži informacije o autoru",
+ "multimediaviewer-thumbnail-error-description": "Izgleda da postoji tehnički problem. Možete $1 ili $3 ako se nastavi. Greška: $2",
+ "multimediaviewer-thumbnail-error-retry": "probati opet",
+ "multimediaviewer-thumbnail-error-report": "prijaviti problem",
+ "multimediaviewer-license-cc-pd": "Javno vlasništvo",
+ "multimediaviewer-license-pd": "Javno vlasništvo",
+ "multimediaviewer-license-default": "Vidi licencu",
+ "multimediaviewer-permission-title": "Detalji dozvole",
+ "multimediaviewer-permission-link": "vidi uslove",
+ "multimediaviewer-permission-viewmore": "Prikaži još",
+ "multimediaviewer-about-mmv": "O pregledniku",
+ "multimediaviewer-help-mmv": "Pomoć",
+ "multimediaviewer-optout-mmv": "Onemogući preglednik multimedijskih datoteka",
+ "multimediaviewer-optin-mmv": "Omogući preglednik multimedijskih datoteka",
+ "multimediaviewer-geolocation": "Lokacija: $1",
+ "multimediaviewer-reuse-link": "Dijeli ili ugnijezdi ovu datoteku",
+ "multimediaviewer-reuse-loading-placeholder": "Učitavam…",
+ "multimediaviewer-share-tab": "Dijeli",
+ "multimediaviewer-embed-tab": "Ugradi",
+ "multimediaviewer-download-link": "Preuzmi ovu datoteku",
+ "multimediaviewer-download-preview-link-title": "Vidi u web pregledniku",
+ "multimediaviewer-download-original-button-name": "Preuzmi originalnu datoteku",
+ "multimediaviewer-download-small-button-name": "Preuzmi u maloj veličini",
+ "multimediaviewer-download-medium-button-name": "Preuzmi u srednjoj veličini",
+ "multimediaviewer-download-large-button-name": "Preuzmi veliku sliku",
+ "multimediaviewer-link-to-page": "Veza na stranicu s informacijama o datoteki",
+ "multimediaviewer-link-to-file": "Veza na izvornu datoteku",
+ "multimediaviewer-share-explanation": "Kopirajte i slobodno dijelite ovu vezu",
+ "multimediaviewer-embed-wt": "Wikitekst",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-embed-explanation": "Koristite ovaj kôd da biste ugradili datoteku",
+ "multimediaviewer-original-embed-dimensions": "Izvorna datoteka $1",
+ "multimediaviewer-large-embed-dimensions": "Velika $1",
+ "multimediaviewer-medium-embed-dimensions": "Srednja $1",
+ "multimediaviewer-small-embed-dimensions": "Mala $1",
+ "multimediaviewer-commons-subtitle": "Spremište slobodnih medijskih datoteka",
+ "multimediaviewer-view-expanded": "Otvori u medijskom pregledniku",
+ "multimediaviewer-view-config": "Konfiguracija",
+ "multimediaviewer-close-popup-text": "Zatvori alat (Esc)",
+ "multimediaviewer-fullscreen-popup-text": "Prikaži preko cijelog ekrana",
+ "multimediaviewer-defullscreen-popup-text": "Izađi iz prikaza preko cijelog ekrana",
+ "multimediaviewer-title-popup-text": "Opis",
+ "multimediaviewer-credit-popup-text": "Autor i informacije o izvoru",
+ "multimediaviewer-title-popup-text-more": "Prikaži cijeli opis",
+ "multimediaviewer-download-attribution-cta-header": "Trebate navesti autora",
+ "multimediaviewer-download-attribution-cta": "Pokaži mi kako",
+ "multimediaviewer-attr-plain": "Običan tekst",
+ "multimediaviewer-options-tooltip": "Omogući ili onemogući preglednik multimedijskih datoteka",
+ "multimediaviewer-options-dialog-header": "Isključite preglednik medija?",
+ "multimediaviewer-options-text-header": "Isključi ovu mogućnost za sve datoteke.",
+ "multimediaviewer-options-text-body": "Naknadno je možete uključiti na stranici s pojedinostima datoteke.",
+ "multimediaviewer-options-learn-more": "Saznaj više",
+ "multimediaviewer-option-submit-button": "Isključite preglednik",
+ "multimediaviewer-option-cancel-button": "Otkaži",
+ "multimediaviewer-disable-confirmation-header": "Isključili ste preglednik medija",
+ "multimediaviewer-disable-confirmation-text": "Sljedeći put kad kliknete na umanjenu sliku na projektu $1, odmah ćete vidjeti pojedinosti datoteke.",
+ "multimediaviewer-enable-dialog-header": "Omogući preglednik multimedijskih datoteka?",
+ "multimediaviewer-enable-text-header": "Uključi ovu mogućnost ubuduće kao podrazumijevanu za sve datoteke.",
+ "multimediaviewer-enable-submit-button": "Uključi preglednik medija",
+ "multimediaviewer-enable-confirmation-header": "Uključili ste preglednik medija za sve datoteke",
+ "multimediaviewer-enable-confirmation-text": "Sljedeći put kad kliknete na smanjenu sliku na projektu $1, koristit će se preglednik.",
+ "multimediaviewer-enable-alert": "Preglednik medija je trenutno isključen",
+ "multimediaviewer-disable-info-title": "Onemogućili ste preglednik multimedijskih datoteka",
+ "multimediaviewer-disable-info": "Još uvijek možete koristiti preglednik multimedijskih datoteka za pojedinačne datoteke."
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/ca.json b/www/wiki/extensions/MultimediaViewer/i18n/ca.json
new file mode 100644
index 00000000..e825e608
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/ca.json
@@ -0,0 +1,83 @@
+{
+ "@metadata": {
+ "authors": [
+ "Fitoschido",
+ "QuimGil",
+ "Vriullop",
+ "Ssola",
+ "Macofe",
+ "Pginer",
+ "F3RaN"
+ ]
+ },
+ "multimediaviewer-desc": "Amplia les miniatures en una interfície a pantalla completa.",
+ "multimediaviewer-pref": "Visualitzador multimèdia",
+ "multimediaviewer-pref-desc": "Milloreu la vostra experiència de visualització multimèdia amb aquesta nova eina. Mostra imatges a mida més gran en les pàgines que tenen miniatures. Les imatges es mostren en una pantalla completa més agradable, i també es poden veure a mida completa.",
+ "multimediaviewer-optin-pref": "Activa el <span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About visor multimèdia]</span>",
+ "multimediaviewer-file-page": "Vés a la pàgina corresponent del fitxer",
+ "multimediaviewer-repository-local": "Més informació",
+ "multimediaviewer-datetime-created": "Creat: $1",
+ "multimediaviewer-datetime-uploaded": "Carregat: $1",
+ "multimediaviewer-metadata-error": "Error: no hem pogut carregar les dades de la imatge. $1",
+ "multimediaviewer-thumbnail-error": "Error: No hem pogut carregar les dades de les miniatures. $1",
+ "multimediaviewer-license-cc-pd": "Domini públic",
+ "multimediaviewer-license-pd": "Domini públic",
+ "multimediaviewer-license-default": "Mostra la llicència",
+ "multimediaviewer-permission-title": "Detalls de la llicència",
+ "multimediaviewer-permission-link": "Vegeu les condicions",
+ "multimediaviewer-permission-viewmore": "Mostra'n més",
+ "multimediaviewer-restriction-2257": "Aquesta imatge conté contingut sexual explícit que pot estar subjecte a la Llei de Protecció d'Infants i l'Obscenitat d'aplicació als Estats Units.",
+ "multimediaviewer-restriction-communist": "Aquesta imatge conté insignes comunistes susceptibles de ser prohibides en determinats països.",
+ "multimediaviewer-restriction-costume": "Aquesta imatge mostra vestuari i és susceptible d'estar subjecte a restriccions legals.",
+ "multimediaviewer-restriction-currency": "Aquesta imatge representa el detall d'una moneda i pot estar subjecte a restriccions legals.",
+ "multimediaviewer-restriction-design": "La configuració temàtica d'aquesta imatge pot contenir drets d'autor i estar subjecte a restriccions legals.",
+ "multimediaviewer-restriction-ihl": "Aquesta imatge conté símbols restringits pel Dret Humanitari Internacional.",
+ "multimediaviewer-restriction-insignia": "Aquesta imatge conté insígnies oficials susceptibles d'estar subjectes a restriccions legals.",
+ "multimediaviewer-restriction-ita-mibac": "Aquesta imatge reprodueix una propietat de patrimoni cultural italià i està restringida per la justícia italiana.",
+ "multimediaviewer-restriction-nazi": "Aquesta imatge conté símbols nazi o feixistes que poden ser prohibits en determinats països.",
+ "multimediaviewer-restriction-personality": "Aquesta imatge conté persones que poden tenir drets que legalment restringeixin determinats re-usos de la imatge sense el seu consentiment.",
+ "multimediaviewer-restriction-default": "Aquesta imatge encara és susceptible de ser restringida degut a altres disposicions legals fora de la llei de propietat intel·lectual. Vegeu la pàgina de descripció del fitxer per a més detalls.",
+ "multimediaviewer-restriction-default-and-others": "Aquesta imatge encara és susceptible de ser restringida degut a altres disposicions legals fora de la llei de propietat intel·lectual. Vegeu la pàgina de descripció del fitxer per a més detalls.",
+ "multimediaviewer-about-mmv": "Quant al visor multimèdia",
+ "multimediaviewer-discuss-mmv": "Comenta aquesta funcionalitat",
+ "multimediaviewer-help-mmv": "Ajuda",
+ "multimediaviewer-optout-mmv": "Desactiva el visor multimèdia",
+ "multimediaviewer-optin-mmv": "Activa el visor multimèdia",
+ "multimediaviewer-optout-pending-mmv": "Desactivant el visor multimèdia",
+ "multimediaviewer-optin-pending-mmv": "Activant el visor multimèdia",
+ "multimediaviewer-optout-help": "Les imatges ja no es mostraran amb el visor multimèdia. Per utilitzar-lo de nou, cliqueu el botó \"Amplia\" al costat d'una imatge. Desprès cliqueu \"{{int:multimediaviewer-optin-mmv}}\".",
+ "multimediaviewer-optin-help": "Les imatges es mostraran amb el visor multimèdia.",
+ "multimediaviewer-geolocation": "Emplaçament: $1",
+ "multimediaviewer-reuse-link": "Utilitza aquest fitxer",
+ "multimediaviewer-reuse-loading-placeholder": "S'està carregant...",
+ "multimediaviewer-share-tab": "Comparteix",
+ "multimediaviewer-embed-tab": "Insereix",
+ "multimediaviewer-download-link": "Descarrega",
+ "multimediaviewer-download-preview-link-title": "Visualitza en el navegador",
+ "multimediaviewer-download-original-button-name": "Descarrega el fitxer original",
+ "multimediaviewer-download-small-button-name": "Descarrega en mida petita",
+ "multimediaviewer-download-medium-button-name": "Descarrega en mida mitjana",
+ "multimediaviewer-download-large-button-name": "Descarrega en mida gran",
+ "multimediaviewer-link-to-page": "Enllaç a la pàgina de descripció del fitxer",
+ "multimediaviewer-link-to-file": "Enllaç al fitxer original",
+ "multimediaviewer-share-explanation": "Copiar i compartir lliurement l'enllaç",
+ "multimediaviewer-embed-wt": "Wikitext",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-embed-explanation": "Utilitzar aquest codi per incrustar l'arxiu",
+ "multimediaviewer-embed-byline": "Per $1",
+ "multimediaviewer-embed-license": "Sota la llicència $1.",
+ "multimediaviewer-embed-via": "Via $1.",
+ "multimediaviewer-default-embed-dimensions": "Mida de la miniatura per defecte",
+ "multimediaviewer-original-embed-dimensions": "Fitxer original $1",
+ "multimediaviewer-large-embed-dimensions": "Gran $1",
+ "multimediaviewer-medium-embed-dimensions": "Mitjana $1",
+ "multimediaviewer-small-embed-dimensions": "Petita $1",
+ "multimediaviewer-description-page-button-text": "Més detalls",
+ "multimediaviewer-description-page-popup-text": "Més informació sobre aquest fitxer a $1",
+ "multimediaviewer-commons-subtitle": "El repositori multimèdia lliure",
+ "multimediaviewer-view-expanded": "Amplia la visualització",
+ "multimediaviewer-close-popup-text": "Tanca aquesta eina (Esc)",
+ "multimediaviewer-fullscreen-popup-text": "Mostra a pantalla completa",
+ "multimediaviewer-defullscreen-popup-text": "Surt del mode de pantalla completa",
+ "multimediaviewer-title-popup-text": "Descripció"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/ce.json b/www/wiki/extensions/MultimediaViewer/i18n/ce.json
new file mode 100644
index 00000000..b7cd70df
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/ce.json
@@ -0,0 +1,95 @@
+{
+ "@metadata": {
+ "authors": [
+ "Умар",
+ "Исмаил Садуев"
+ ]
+ },
+ "multimediaviewer-desc": "Схьадосту суьрташ юьззина экран.",
+ "multimediaviewer-pref": "Медиа-хьожург",
+ "multimediaviewer-pref-desc": "Мультимедиа-файлашка хьажар хаза кечдина гойту.",
+ "multimediaviewer-optin-pref": "Латае <span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About Медиа-хьожург]</span>",
+ "multimediaviewer-file-page": "Йолуш йолу файлан агӀонга дехьавала",
+ "multimediaviewer-repository-local": "Ма-дарра",
+ "multimediaviewer-datetime-created": "Кхоьллина: $1",
+ "multimediaviewer-datetime-uploaded": "Чуяьккхина: $1",
+ "multimediaviewer-credit-fallback": "Авторах лаьцна хьажа",
+ "multimediaviewer-multiple-authors-combine": "$1 $2 тӀе",
+ "multimediaviewer-metadata-error": "ГӀалат: Суьртан хаамаш гайта цабелира. $1",
+ "multimediaviewer-thumbnail-error": "ГӀалат: Эскизан хаамаш гайта цабелира. $1",
+ "multimediaviewer-thumbnail-error-description": "Техникин гӀалат даьллачух тера ду. Из гӀалат юха а хилахь хьан йиш ю $1 я $3. ГӀалат: $2",
+ "multimediaviewer-thumbnail-error-retry": "юхагӀорта",
+ "multimediaviewer-thumbnail-error-report": "гӀалатех хаам бар",
+ "multimediaviewer-license-cc-pd": "Юкъараллин рицӀкъ",
+ "multimediaviewer-license-pd": "Юкъараллин рицӀкъ",
+ "multimediaviewer-license-default": "Лицензига хьажар",
+ "multimediaviewer-permission-title": "Лецензех лаьцна",
+ "multimediaviewer-permission-link": "хьоле хьажар",
+ "multimediaviewer-permission-viewmore": "Хьажа мадарра",
+ "multimediaviewer-about-mmv": "Медиа-хьожучух лаьцна",
+ "multimediaviewer-discuss-mmv": "Йийца хӀара функци",
+ "multimediaviewer-help-mmv": "ГӀо",
+ "multimediaviewer-optout-mmv": "ДӀаяйа Медиа-хьожург",
+ "multimediaviewer-optin-mmv": "Латае медиа-хьожург",
+ "multimediaviewer-optout-pending-mmv": "ДӀаяйа медиа-хьожург",
+ "multimediaviewer-optin-pending-mmv": "Медиа-хьожург латор",
+ "multimediaviewer-optout-help": "Медиа-хьожург кхий лелош яц. Юху и лелаян муьлху а суьртан уллера кнопка тӀетаӀае \"{{INT: multimediaviewer-Optin-MMV}}\".",
+ "multimediaviewer-optin-help": "ХӀинца медиа-хьожург лелор ю суьрташка хьожуш.",
+ "multimediaviewer-geolocation": "Географин йолу меттиг: $1",
+ "multimediaviewer-reuse-link": "Лелае хӀара файл",
+ "multimediaviewer-reuse-loading-placeholder": "Чуйолуш...",
+ "multimediaviewer-share-tab": "Екха",
+ "multimediaviewer-embed-tab": "АгӀонг йилла",
+ "multimediaviewer-download-link": "Чуяккха хӀара файл",
+ "multimediaviewer-download-preview-link-title": "Браузер чохь хьажар",
+ "multimediaviewer-download-original-button-name": "ДӀайолалун барамехь схьаэца",
+ "multimediaviewer-download-small-button-name": "Схьаэца жима барамехь",
+ "multimediaviewer-download-medium-button-name": "Схьаэца юкъара барамехь",
+ "multimediaviewer-download-large-button-name": "Схьаэца йоккха барамехь",
+ "multimediaviewer-link-to-page": "Файлах лаьцна дерг долу агӀона тӀе хьажорг",
+ "multimediaviewer-link-to-file": "ДӀайолалун файлан тӀе хьажорг",
+ "multimediaviewer-share-explanation": "Копи яккхин маьрша лелае хьажорг",
+ "multimediaviewer-embed-wt": "Викийоза",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-embed-explanation": "Лелае хӀара код файл агӀонг юьлуш",
+ "multimediaviewer-embed-byline": "Декъашхой $1",
+ "multimediaviewer-embed-license": "Лицензица $1.",
+ "multimediaviewer-embed-via": "$1 чухула.",
+ "multimediaviewer-default-embed-dimensions": "Эскизан Ӏадйитаран кепаца болу барам",
+ "multimediaviewer-original-embed-dimensions": "ДӀайолалун файл $1",
+ "multimediaviewer-large-embed-dimensions": "Йоккханиг $1",
+ "multimediaviewer-medium-embed-dimensions": "Юккъерниг $1",
+ "multimediaviewer-small-embed-dimensions": "Жимниг $1",
+ "multimediaviewer-description-page-button-text": "Мадарра",
+ "multimediaviewer-description-page-popup-text": "Мадарра ду $1 чохь",
+ "multimediaviewer-commons-subtitle": "Медиа-файлаш латайо меттиг",
+ "multimediaviewer-view-expanded": "Схьадаста хьажар",
+ "multimediaviewer-view-config": "Нисяр",
+ "multimediaviewer-close-popup-text": "ДӀачӀагӀа хӀара гӀирс (Esc)",
+ "multimediaviewer-fullscreen-popup-text": "Йокха гайта",
+ "multimediaviewer-defullscreen-popup-text": "Жима гайта",
+ "multimediaviewer-title-popup-text": "Цуьнах лаьцна",
+ "multimediaviewer-credit-popup-text": "Авторах а хьостах а лаьцна хаам",
+ "multimediaviewer-title-popup-text-more": "Хьажа цунах лаьцна мадарра",
+ "multimediaviewer-credit-popup-text-more": "Авторах а, хьостах а лаьцна мадарра хьажа",
+ "multimediaviewer-download-attribution-cta-header": "ДӀаязъе авторан цӀе",
+ "multimediaviewer-download-attribution-cta": "Гайта соьга, санна",
+ "multimediaviewer-attr-plain": "Атта",
+ "multimediaviewer-options-tooltip": "Латор я дӀаяйар Медиа-хьожург",
+ "multimediaviewer-options-dialog-header": "ДӀаяйа Медиа-хьожург?",
+ "multimediaviewer-options-text-header": "Латае хӀара хьажаран функци массо файлашна.",
+ "multimediaviewer-options-text-body": "Хьан йиш ю и тӀаьхьо латаян файлах лаьцна хаам болу агӀонгахь.",
+ "multimediaviewer-options-learn-more": "Цул совнаха хаа",
+ "multimediaviewer-option-submit-button": "ДӀаяйа Медиа-хьожург",
+ "multimediaviewer-option-cancel-button": "Цаоьшу",
+ "multimediaviewer-disable-confirmation-header": "Ахьа дӀаяйина Медиа-хьожург",
+ "multimediaviewer-disable-confirmation-text": "Кхечу хенахь $1 сайтехь файлан тӀе таӀийча хьана файлан хаам хьажа йиш хира ю.",
+ "multimediaviewer-enable-dialog-header": "Латае медиа-хьожург?",
+ "multimediaviewer-enable-text-header": "Латае медиа-хьожург массо файлашна.",
+ "multimediaviewer-enable-submit-button": "Латае медиа-хьожург",
+ "multimediaviewer-enable-confirmation-header": "Ахьа латийна медиа-хьожург массо файлашна.",
+ "multimediaviewer-enable-confirmation-text": "Кхечу хенахь $1 сайтехь файлан тӀе таӀийча хьана медиа-хьожург лелор ю.",
+ "multimediaviewer-enable-alert": "Медиа-хьожург хӀинца яйина ю",
+ "multimediaviewer-disable-info-title": "Ахьа дӀаяйина Медиа-хьожург",
+ "multimediaviewer-disable-info": "Хьа хӀинца а йиш ю цхьайолу файлашка медиа-хьожургцан хьажа."
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/ckb.json b/www/wiki/extensions/MultimediaViewer/i18n/ckb.json
new file mode 100644
index 00000000..6809e11f
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/ckb.json
@@ -0,0 +1,51 @@
+{
+ "@metadata": {
+ "authors": [
+ "Asoxor",
+ "Pirehelokan",
+ "Sarchia",
+ "Lost Whispers",
+ "Épine"
+ ]
+ },
+ "multimediaviewer-pref": "نیشاندەری میدیا",
+ "multimediaviewer-repository-local": "وردەکاریی زیاتر",
+ "multimediaviewer-datetime-created": "دروست کراوە لە: $1",
+ "multimediaviewer-datetime-uploaded": "بارکراوە لە: $1",
+ "multimediaviewer-credit-fallback": "زانیاری سەبارەت بە بەرھەمھێنەر",
+ "multimediaviewer-multiple-authors-combine": "$1 و $2",
+ "multimediaviewer-thumbnail-error-retry": "ھەوڵدانەوە",
+ "multimediaviewer-license-cc-pd": "پاوانی گشتی",
+ "multimediaviewer-license-pd": "پاوانی گشتی",
+ "multimediaviewer-license-default": "مۆڵەتنامەکەی ببینە",
+ "multimediaviewer-permission-viewmore": "زیاتر ببینە",
+ "multimediaviewer-restriction-communist": "ئەم وێنەیە ئاڵای کۆمۆنیستی تێدایە کە ڕەنگە لە بڕێک وڵاتەکاندا قەدەغەکراو بێت.",
+ "multimediaviewer-restriction-costume": "ئەم وێنەیە کاستومینگ پیشان دەدات و ڕەنگە کێشەی یاسایی ھەبێت.",
+ "multimediaviewer-restriction-currency": "ئەم پەڕەیە یەکینەی پارەیەک پیشان دەدات کە ڕەنگە قەدەغەکراو بێت.",
+ "multimediaviewer-restriction-design": " ڕەنگە داڕشتنی ناوەرۆکی ئەم وێنەیە لە ژێر مافی کۆپیدا بێت و کێشەی یاسایی ھەبێت.",
+ "multimediaviewer-restriction-fan-art": "ئەم وێنەیە کاری ھۆگرێکی ھونەرە و ڕەنگە بەکارھێنانەوەی کێشەی یاسایی ھەبێت.",
+ "multimediaviewer-restriction-ihl": "ئەم وێنەیە بڕێک نیشانەی تێدایە کە لە لایەن یاسای نێودەوڵەتیی مرۆڤایەتییەوە قەدەغە کراوە.",
+ "multimediaviewer-restriction-nazi": "ئەم وێنەیە ئاڵای نازی یان ناوەرۆکی فاشیستیی تری تێدایە کە ڕەنگە لە بڕێک وڵاتەکاندا قەدەغەکراو بێت.",
+ "multimediaviewer-restriction-default": "ڕەنگە ئەم وێنەیە کێشەی یاسایی ببێت سەبارەت بە مافی کۆپی کردنەوە. بۆ زانیاریی زیاتر پەڕەی وەسفی فایلەکە ببینە.",
+ "multimediaviewer-discuss-mmv": "وتووێژ",
+ "multimediaviewer-help-mmv": "یارمەتی",
+ "multimediaviewer-optin-mmv": "چالاککردنی نیشاندەری میدیا",
+ "multimediaviewer-geolocation": "شوێن: $1",
+ "multimediaviewer-reuse-loading-placeholder": "بارکردن...",
+ "multimediaviewer-download-link": "ئەم پەڕگەیە دابگرە",
+ "multimediaviewer-embed-via": "لە ڕێگەی $1ـەوە.",
+ "multimediaviewer-large-embed-dimensions": "گەورە $1",
+ "multimediaviewer-medium-embed-dimensions": "نێونجی $1",
+ "multimediaviewer-small-embed-dimensions": "بچووک $1",
+ "multimediaviewer-view-expanded": "کردنەوە لە نیشاندەری میدیادا",
+ "multimediaviewer-close-popup-text": "ئەم ئامرازە دا بخە (Esc)",
+ "multimediaviewer-title-popup-text": "وەسف",
+ "multimediaviewer-credit-popup-text": "زانیاریی بەرھەمھێنەر و سەرچاوە",
+ "multimediaviewer-title-popup-text-more": "وەسفی تەواو ببینە",
+ "multimediaviewer-credit-popup-text-more": "بەرھەمھێنەر و سەرچاوە بە تەواوی ببینە",
+ "multimediaviewer-download-attribution-cta": "نیشانم بدە چۆن",
+ "multimediaviewer-attr-plain": "ئاسایی",
+ "multimediaviewer-options-learn-more": "زۆرتر بزانە",
+ "multimediaviewer-enable-dialog-header": "چالاککردنی نیشاندەری میدیا؟",
+ "multimediaviewer-enable-submit-button": "چالاککردنی نیشاندەری میدیا"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/cs.json b/www/wiki/extensions/MultimediaViewer/i18n/cs.json
new file mode 100644
index 00000000..6220ad2a
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/cs.json
@@ -0,0 +1,139 @@
+{
+ "@metadata": {
+ "authors": [
+ "Mormegil",
+ "Paxt",
+ "Rosnicka.kacka",
+ "Utar",
+ "Dominikmatus",
+ "Littledogboy",
+ "Macofe"
+ ]
+ },
+ "multimediaviewer-desc": "Zvětší náhledy obrázků do celoobrazovkového rozhraní.",
+ "multimediaviewer-pref": "Prohlížeč médií",
+ "multimediaviewer-pref-desc": "Pomocí tohoto nástroje si můžete zpříjemnit prohlížení multimédií. Na stránkách, na kterých se používají náhledy obrázků, umožňuje prohlížení těchto obrázků ve větší velikosti. Obrázky se zobrazí v hezčím celoobrazovkovém rozhraní a lze si je prohlédnout také v plné velikosti.",
+ "multimediaviewer-optin-pref": "Aktivovat <span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About Prohlížeč médií]</span>",
+ "multimediaviewer-file-page": "Přejít na stránku s popisem souboru",
+ "multimediaviewer-repository-local": "Více informací",
+ "multimediaviewer-datetime-created": "Vytvořeno: $1",
+ "multimediaviewer-datetime-uploaded": "Načteno: $1",
+ "multimediaviewer-credit": "$1 – $2",
+ "multimediaviewer-credit-fallback": "Zobrazit informace o autorovi",
+ "multimediaviewer-multiple-authors": "{{PLURAL:$1|jeden další autor|$1 další autoři|$1 dalších autorů}}",
+ "multimediaviewer-multiple-authors-combine": "$1 a $2",
+ "multimediaviewer-metadata-error": "Nelze načíst podrobnosti k obrázku (chyba: $1)",
+ "multimediaviewer-thumbnail-error": "Je nám líto, soubor nelze zobrazit",
+ "multimediaviewer-thumbnail-error-description": "Vypadá to na technické potíže. Můžete to $1 nebo $3, pokud se to nezlepší. Chyba: $2",
+ "multimediaviewer-thumbnail-error-retry": "zkusit znovu",
+ "multimediaviewer-thumbnail-error-report": "problém nahlásit",
+ "multimediaviewer-license-cc-pd": "Volné dílo",
+ "multimediaviewer-license-pd": "Volné dílo",
+ "multimediaviewer-license-default": "Zobrazit licenci",
+ "multimediaviewer-permission-title": "Podrobnosti o svolení",
+ "multimediaviewer-permission-link": "zobrazit podmínky",
+ "multimediaviewer-permission-link-hide": "skrýt podmínky",
+ "multimediaviewer-permission-viewmore": "Zobrazit více",
+ "multimediaviewer-restriction-2257": "Tento obrázek zahrnuje sexuálně explicitní obsah, který může být ve Spojených státech předmětem zákona Child Protection and Obscenity Enforcement Act",
+ "multimediaviewer-restriction-aus-reserve": "Tento obrázek byl vyfotografován v australské přírodní rezervaci a je zakázáno užívat ho ke komerčním účelům bez povolení.",
+ "multimediaviewer-restriction-communist": "Tento obrázek obsahuje komunistické symboly, které mohou být v některých zemích zakázány.",
+ "multimediaviewer-restriction-costume": "Tento soubor vyobrazuje kostým a může být předmětem zákonných omezení.",
+ "multimediaviewer-restriction-currency": "Tento soubor vyobrazuje platidlo a může být předmětem zákonných omezení.",
+ "multimediaviewer-restriction-design": "Design předmětu tohoto obrázku může být chráněn autorským právem či podléhat jiným právním omezením.",
+ "multimediaviewer-restriction-fan-art": "Tento obrázek je dílo typu „fan art“ a jeho další užití může být předmětem zákonných omezení.",
+ "multimediaviewer-restriction-ihl": "Tento obrázek obsahuje symboly omezené mezinárodním humanitárním právem.",
+ "multimediaviewer-restriction-insignia": "Tento obrázek obsahuje oficiální znak, který může být předmětem zákonných omezení.",
+ "multimediaviewer-restriction-ita-mibac": "Tento obrázek reprodukuje majetek patřící mezi italské kulturní památky a je omezen italskými zákony.",
+ "multimediaviewer-restriction-nazi": "Tento obrázek obsahuje nacistické nebo jiné fašistické symboly, které mohou být v některých zemích zakázány.",
+ "multimediaviewer-restriction-personality": "Tento obrázek obsahuje osoby, které mohou mít práva omezující některá užití tohoto obrázku bez svolení.",
+ "multimediaviewer-restriction-trademarked": "Obsah tohoto obrázku může být předmětem známkoprávní ochrany.",
+ "multimediaviewer-restriction-default": "Tento obrázek může být omezen právními předpisy mimo autorské právo. Podrobnosti najdete na stránce s popisem souboru.",
+ "multimediaviewer-restriction-default-and-others": "Tento obrázek může být dále omezen jinými právními předpisy mimo autorské právo. Podrobnosti najdete na stránce s popisem souboru.",
+ "multimediaviewer-about-mmv": "O prohlížeči",
+ "multimediaviewer-discuss-mmv": "Diskuse",
+ "multimediaviewer-help-mmv": "Nápověda",
+ "multimediaviewer-optout-mmv": "Vypnout Prohlížeč médií",
+ "multimediaviewer-optin-mmv": "Zapnout Prohlížeč médií",
+ "multimediaviewer-optout-pending-mmv": "Prohlížeč médií se vypíná",
+ "multimediaviewer-optin-pending-mmv": "Prohlížeč médií se zapíná",
+ "multimediaviewer-optout-help": "K zobrazování obrázků se již nebude používat Prohlížeč médií. Pokud ho budete chtít znovu použít, klikněte na tlačítko „{{int:multimediaviewer-view-expanded}}“ u libovolného obrázku. Poté klikněte na „{{int:multimediaviewer-optin-mmv}}“.",
+ "multimediaviewer-optin-help": "K zobrazování obrázků se bude používat Prohlížeč médií.",
+ "multimediaviewer-geoloc-north": "s.&nbsp;š.",
+ "multimediaviewer-geoloc-east": "v.&nbsp;d.",
+ "multimediaviewer-geoloc-south": "j.&nbsp;š.",
+ "multimediaviewer-geoloc-west": "z.&nbsp;d.",
+ "multimediaviewer-geolocation": "Místo: $1",
+ "multimediaviewer-reuse-link": "Sdílet nebo vložit tento soubor",
+ "multimediaviewer-reuse-loading-placeholder": "Načítám…",
+ "multimediaviewer-reuse-copy-share": "Vybrat a zkopírovat (pokud je podporováno) odkaz pro sdílení tohoto souboru",
+ "multimediaviewer-reuse-copy-embed": "Vybrat a zkopírovat (pokud je podporováno) odkaz pro vložení tohoto souboru",
+ "multimediaviewer-share-tab": "Sdílet",
+ "multimediaviewer-embed-tab": "Vložit",
+ "multimediaviewer-download-link": "Stáhnout tento soubor",
+ "multimediaviewer-download-preview-link-title": "Zobrazit v prohlížeči",
+ "multimediaviewer-download-original-button-name": "Stáhnout původní soubor",
+ "multimediaviewer-download-small-button-name": "Stáhnout v malé velikosti",
+ "multimediaviewer-download-medium-button-name": "Stáhnout ve střední velikosti",
+ "multimediaviewer-download-large-button-name": "Stáhnout ve velké velikosti",
+ "multimediaviewer-link-to-page": "Odkaz na stránku s popisem souboru",
+ "multimediaviewer-link-to-file": "Odkaz na původní soubor",
+ "multimediaviewer-share-explanation": "Kopírujte a volně sdílejte odkaz",
+ "multimediaviewer-embed-wt": "Wikitext",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-embed-explanation": "K vložení tohoto souboru použijte tento kód",
+ "multimediaviewer-text-embed-credit-text-bl": "Autor: $1, $2, $3",
+ "multimediaviewer-text-embed-credit-text-b": "Autor: $1, $2",
+ "multimediaviewer-html-embed-credit-text-bl": "Autor: $1, $2, $3",
+ "multimediaviewer-html-embed-credit-text-b": "Autor: $1, $2",
+ "multimediaviewer-html-embed-credit-link-text": "Odkaz",
+ "multimediaviewer-embed-byline": "Od $1",
+ "multimediaviewer-embed-license": "Licencováno pod $1.",
+ "multimediaviewer-embed-via": "Via $1.",
+ "multimediaviewer-default-embed-dimensions": "Výchozí velikost náhledu",
+ "multimediaviewer-original-embed-dimensions": "Původní soubor $1",
+ "multimediaviewer-large-embed-dimensions": "Velký $1",
+ "multimediaviewer-medium-embed-dimensions": "Střední $1",
+ "multimediaviewer-small-embed-dimensions": "Malý $1",
+ "multimediaviewer-embed-dimensions-separated": "– $1",
+ "multimediaviewer-description-page-button-text": "Více informací o tomto souboru",
+ "multimediaviewer-description-page-popup-text": "Více informací o tomto souboru na {{grammar:6sg|$1}}",
+ "multimediaviewer-commons-subtitle": "Úložiště volných médií",
+ "multimediaviewer-view-expanded": "Otevřít v prohlížeči médií",
+ "multimediaviewer-view-config": "Konfigurace",
+ "multimediaviewer-close-popup-text": "Zavřít tento nástroj (Esc)",
+ "multimediaviewer-fullscreen-popup-text": "Zobrazit přes celou obrazovku",
+ "multimediaviewer-defullscreen-popup-text": "Ukončit celou obrazovku",
+ "multimediaviewer-next-image-alt-text": "Zobrazit následující obrázek",
+ "multimediaviewer-prev-image-alt-text": "Zobrazit předchozí obrázek",
+ "multimediaviewer-title-popup-text": "Popis",
+ "multimediaviewer-credit-popup-text": "Informace o autorovi a zdroji",
+ "multimediaviewer-title-popup-text-more": "Zobrazit úplný popis",
+ "multimediaviewer-credit-popup-text-more": "Zobrazit úplné informace o autorovi a zdroji",
+ "multimediaviewer-download-attribution-cta-header": "Musíte uvést autora",
+ "multimediaviewer-download-optional-attribution-cta-header": "Můžete uvést autora",
+ "multimediaviewer-download-attribution-cta": "Ukažte mi, jak",
+ "multimediaviewer-download-attribution-copy": "Vybrat a zkopírovat (pokud je podporováno) text uvádějící autorství tohoto souboru",
+ "multimediaviewer-reuse-warning-deletion": "Tento soubor byl navržen na smazání.",
+ "multimediaviewer-reuse-warning-nonfree": "Tento soubor nemá svobodnou licenci.",
+ "multimediaviewer-reuse-warning-noattribution": "Tento soubor nemá informace o autorovi.",
+ "multimediaviewer-reuse-warning-generic": "Před jeho použitím zkontrolujte [$1 podrobnosti].",
+ "multimediaviewer-attr-plain": "Prostý text",
+ "multimediaviewer-options-tooltip": "Zapnout nebo vypnout Prohlížeč médií",
+ "multimediaviewer-options-dialog-header": "Vypnout Prohlížeč médií?",
+ "multimediaviewer-options-text-header": "Vypnout tento prohlížeč u všech souborů.",
+ "multimediaviewer-options-text-body": "Později ho můžete zapnout na stránce s popisem souboru.",
+ "multimediaviewer-options-learn-more": "Více informací",
+ "multimediaviewer-option-submit-button": "Vypnout Prohlížeč médií",
+ "multimediaviewer-option-cancel-button": "Storno",
+ "multimediaviewer-disable-confirmation-header": "Vypnuli jste Prohlížeč médií",
+ "multimediaviewer-disable-confirmation-text": "Když příště na {{grammar:6sg|$1}} kliknete na náhled, zobrazí se rovnou všechny podrobnosti o souboru.",
+ "multimediaviewer-enable-dialog-header": "Zapnout Prohlížeč médií?",
+ "multimediaviewer-enable-text-header": "Zapnout tento prohlížeč pro implicitní zobrazení všech souborů.",
+ "multimediaviewer-enable-submit-button": "Zapnout Prohlížeč médií",
+ "multimediaviewer-enable-confirmation-header": "Zapnuli jste Prohlížeč médií pro všechny soubory",
+ "multimediaviewer-enable-confirmation-text": "Když příště na {{grammar:6sg|$1}} kliknete na náhled, použije se Prohlížeč médií.",
+ "multimediaviewer-enable-alert": "Prohlížeč médií je teď vypnut",
+ "multimediaviewer-disable-info-title": "Vypnuli jste Prohlížeč médií",
+ "multimediaviewer-disable-info": "I nadále si můžete Prohlížečem médií prohlížet jednotlivé soubory.",
+ "multimediaviewer-errorreport-privacywarning": "Podrobnosti o chybě jsou připojeny k hlášení, které bude veřejně dostupné. Pokud se vám to nelíbí, můžete níže hlášení upravit a odstranit všechny údaje, které nechcete sdílet."
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/cu.json b/www/wiki/extensions/MultimediaViewer/i18n/cu.json
new file mode 100644
index 00000000..933cb0a6
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/cu.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "ОйЛ"
+ ]
+ },
+ "multimediaviewer-discuss-mmv": "бєсѣда"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/cy.json b/www/wiki/extensions/MultimediaViewer/i18n/cy.json
new file mode 100644
index 00000000..ecc06379
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/cy.json
@@ -0,0 +1,27 @@
+{
+ "@metadata": {
+ "authors": [
+ "Lloffiwr",
+ "Robin Owain"
+ ]
+ },
+ "multimediaviewer-datetime-created": "Gwnaethpwyd: $1",
+ "multimediaviewer-datetime-uploaded": "Uwchlwythwyd: $1",
+ "multimediaviewer-license-cc-pd": "Parth Cyhoeddus",
+ "multimediaviewer-license-pd": "Parth Cyhoeddus",
+ "multimediaviewer-license-default": "Gweld y drwydded",
+ "multimediaviewer-permission-title": "Manylion hawliau",
+ "multimediaviewer-permission-link": "gweld y termau",
+ "multimediaviewer-permission-viewmore": "Gweld mwy",
+ "multimediaviewer-about-mmv": "Am Media Viewer",
+ "multimediaviewer-discuss-mmv": "Trafod y nodwedd hon",
+ "multimediaviewer-help-mmv": "Cymorth",
+ "multimediaviewer-optout-mmv": "Analluoger Media Viewer",
+ "multimediaviewer-optin-mmv": "Galluoger Media Viewer",
+ "multimediaviewer-optout-pending-mmv": "Wrthi'n analluogi Media Viewer",
+ "multimediaviewer-optin-pending-mmv": "Wrthi'n galluogi Media Viewer",
+ "multimediaviewer-geolocation": "Lleoliad: $1",
+ "multimediaviewer-reuse-loading-placeholder": "Wrthi'n llwytho",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-view-expanded": "Agorer yn Media Viewer"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/da.json b/www/wiki/extensions/MultimediaViewer/i18n/da.json
new file mode 100644
index 00000000..52b60fee
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/da.json
@@ -0,0 +1,20 @@
+{
+ "@metadata": {
+ "authors": [
+ "Christian List"
+ ]
+ },
+ "multimediaviewer-desc": "Udvid miniaturebilleder i en større størrelse på en fuld skærm.",
+ "multimediaviewer-pref": "Medieviser",
+ "multimediaviewer-pref-desc": "Med dette nye værktøj kan du forbedre din multimedieoplevelse. Det viser billeder i større størrelse på sider, der har miniaturer. Billederne er vist i et pænere fuldskærmsformat, og kan også ses i fuld størrelse.",
+ "multimediaviewer-file-page": "Gå til tilsvarende filside",
+ "multimediaviewer-repository-local": "Lær mere",
+ "multimediaviewer-datetime-created": "Oprettet den $1",
+ "multimediaviewer-datetime-uploaded": "Uploadet den $1",
+ "multimediaviewer-license-cc-pd": "Offentlig ejendom",
+ "multimediaviewer-license-default": "Se licens",
+ "multimediaviewer-about-mmv": "Om Medieviser",
+ "multimediaviewer-discuss-mmv": "Giv feedback",
+ "multimediaviewer-geolocation": "Sted: $1",
+ "multimediaviewer-reuse-link": "Brug denne fil"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/de-ch.json b/www/wiki/extensions/MultimediaViewer/i18n/de-ch.json
new file mode 100644
index 00000000..9de2aacd
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/de-ch.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Filzstift"
+ ]
+ },
+ "multimediaviewer-desc": "Ermöglicht die Darstellung von Vorschaubildern in einer Vollbildschnittstelle"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/de-formal.json b/www/wiki/extensions/MultimediaViewer/i18n/de-formal.json
new file mode 100644
index 00000000..f7ed3027
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/de-formal.json
@@ -0,0 +1,10 @@
+{
+ "@metadata": {
+ "authors": [
+ "Kghbln"
+ ]
+ },
+ "multimediaviewer-optout-help": "Der Medienbetrachter wird nicht mehr zur Anzeige von Bildern verwendet. Um ihn wieder zu nutzen, klicken Sie zunächst auf die Schaltfläche „{{int:multimediaviewer-view-expanded}}“ neben einem beliebigen Bild. Klicken Sie dann auf „{{int:multimediaviewer-optin-mmv}}“.",
+ "multimediaviewer-download-attribution-cta-header": "Sie müssen den Urheber angeben",
+ "multimediaviewer-download-attribution-cta": "Zeigen Sie mir wie"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/de.json b/www/wiki/extensions/MultimediaViewer/i18n/de.json
new file mode 100644
index 00000000..cc364404
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/de.json
@@ -0,0 +1,137 @@
+{
+ "@metadata": {
+ "authors": [
+ "Kghbln",
+ "Metalhead64",
+ "Snatcher",
+ "Inkowik",
+ "Steinsplitter",
+ "Keegan",
+ "WhatamIdoing",
+ "DerHexer",
+ "Envlh"
+ ]
+ },
+ "multimediaviewer-desc": "Ermöglicht die Darstellung von Vorschaubildern in einer Vollbildschnittstelle",
+ "multimediaviewer-pref": "Medienbetrachter",
+ "multimediaviewer-pref-desc": "Dieses neue Werkzeug ändert die Ansicht von Multimediadateien. Es zeigt Bilder auf Seiten größer an, die Vorschaubilder haben. Die Bilder werden in einem den Artikel überlagernden Kasten angezeigt und können auch als Vollbild dargestellt werden.",
+ "multimediaviewer-optin-pref": "Den <span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About Medienbetrachter]</span> nutzen",
+ "multimediaviewer-file-page": "Gehe zur zugehörigen Dateiseite",
+ "multimediaviewer-repository-local": "Weitere Einzelheiten",
+ "multimediaviewer-datetime-created": "Erstellt: $1",
+ "multimediaviewer-datetime-uploaded": "Hochgeladen: $1",
+ "multimediaviewer-credit-fallback": "Autoreninformationen ansehen",
+ "multimediaviewer-multiple-authors": "{{PLURAL:$1|ein weiterer|$1 weitere}} Urheber",
+ "multimediaviewer-multiple-authors-combine": "$1 und $2",
+ "multimediaviewer-metadata-error": "Die Einzelheiten des Bildes konnten nicht geladen werden (Fehler: $1)",
+ "multimediaviewer-thumbnail-error": "Leider kann die Datei nicht angezeigt werden",
+ "multimediaviewer-thumbnail-error-description": "Es scheint ein technisches Problem zu geben. Du kannst es $1 oder $3, falls es weiterhin besteht. Fehler: $2",
+ "multimediaviewer-thumbnail-error-retry": "erneut versuchen",
+ "multimediaviewer-thumbnail-error-report": "das Problem melden",
+ "multimediaviewer-license-cc-by-4.0": "CC-BY 4.0",
+ "multimediaviewer-license-cc-by-sa-4.0": "CC BY-SA 4.0",
+ "multimediaviewer-license-cc-pd": "Gemeinfrei",
+ "multimediaviewer-license-pd": "Gemeinfrei",
+ "multimediaviewer-license-default": "Lizenz ansehen",
+ "multimediaviewer-permission-title": "Einzelheiten zur Genehmigung",
+ "multimediaviewer-permission-link": "Hinweise zur Weiternutzung",
+ "multimediaviewer-permission-link-hide": "Weiternutzungshinweise ausblenden",
+ "multimediaviewer-permission-viewmore": "Mehr",
+ "multimediaviewer-restriction-2257": "Dieses Bild enthält eindeutigen sexuellen Inhalt, der Gegenstand des „Child Protection and Obscenity Enforcement Act“ der Vereinigten Staaten sein könnte.",
+ "multimediaviewer-restriction-aus-reserve": "Dieses Bild wurde in einem australischen Commonwealth-Reservat fotografiert und kann ohne Genehmigung nicht zu kommerziellen Zwecken verwendet werden.",
+ "multimediaviewer-restriction-communist": "Dieses Bild enthält kommunistische Abzeichen, die in einigen Staaten verboten sein könnten.",
+ "multimediaviewer-restriction-costume": "Dieses Bild stellt Kostümierungen dar und könnte Gegenstand rechtlicher Beschränkungen sein.",
+ "multimediaviewer-restriction-currency": "Dieses Bild stellt eine Abbildung einer Währungseinheit dar und könnte Gegenstand rechtlicher Beschränkungen sein.",
+ "multimediaviewer-restriction-design": "Das Design des Gegenstands dieses Bildes könnte urheberrechtlich geschützt und Thema rechtlicher Beschränkungen sein.",
+ "multimediaviewer-restriction-fan-art": "Dieses Bild ist ein Werk von Fankunst und die Weiterverwendung könnte Gegenstand rechtlicher Beschränkungen sein.",
+ "multimediaviewer-restriction-ihl": "Dieses Bild enthält Symbole, die durch internationales humanitäres Recht geschützt sind.",
+ "multimediaviewer-restriction-insignia": "Dieses Bild enthält offizielle Abzeichen, die Gegenstand rechtlicher Beschränkungen sein könnten.",
+ "multimediaviewer-restriction-ita-mibac": "Dieses Bild reproduziert ein Eigentum des italienischen Kulturerbes und ist durch italienisches Recht beschränkt.",
+ "multimediaviewer-restriction-nazi": "Dieses Bild enthält nationalsozialistische oder andere faschistische Abzeichen, die in einigen Staaten verboten sein könnten.",
+ "multimediaviewer-restriction-personality": "Die Persönlichkeitsrechte der abgebildeten Person(en) beschränken bestimmte Weiterverwendungen des Bildes ohne dessen/deren vorherige Zustimmung.",
+ "multimediaviewer-restriction-trademarked": "Dieses Bild enthält einen Inhalt, der Thema von Markenrechten sein könnte.",
+ "multimediaviewer-restriction-default": "Dieses Bild könnte durch rechtliche Bestimmungen außerhalb des Urheberrechts beschränkt sein. Siehe die Dateibeschreibungsseite für Einzelheiten.",
+ "multimediaviewer-restriction-default-and-others": "Dieses Bild könnte durch andere rechtliche Bestimmungen außerhalb des Urheberrechts beschränkt sein. Siehe die Dateibeschreibungsseite für Einzelheiten.",
+ "multimediaviewer-about-mmv": "Über",
+ "multimediaviewer-discuss-mmv": "Diskutieren",
+ "multimediaviewer-help-mmv": "Hilfe",
+ "multimediaviewer-optout-mmv": "Medienbetrachter deaktivieren",
+ "multimediaviewer-optin-mmv": "Medienbetrachter aktivieren",
+ "multimediaviewer-optout-pending-mmv": "Deaktiviere den Medienbetrachter …",
+ "multimediaviewer-optin-pending-mmv": "Aktiviere den Medienbetrachter …",
+ "multimediaviewer-optout-help": "Der Medienbetrachter wird nicht mehr zur Anzeige von Bildern verwendet. Um ihn wieder zu nutzen, klicke zunächst auf die Schaltfläche „{{int:multimediaviewer-view-expanded}}“ neben einem beliebigen Bild. Klicke dann auf „{{int:multimediaviewer-optin-mmv}}“.",
+ "multimediaviewer-optin-help": "Der Medienbetrachter wird zur Anzeige von Bildern verwendet.",
+ "multimediaviewer-geolocation": "Standort: $1",
+ "multimediaviewer-reuse-link": "Diese Datei teilen oder einbetten",
+ "multimediaviewer-reuse-loading-placeholder": "Lade …",
+ "multimediaviewer-reuse-copy-share": "Wähle den Link zum Teilen dieser Datei aus und kopiere ihn (falls unterstützt)",
+ "multimediaviewer-reuse-copy-embed": "Wähle den Code zum Einbetten dieser Datei aus und kopiere ihn (falls unterstützt)",
+ "multimediaviewer-share-tab": "Teilen",
+ "multimediaviewer-embed-tab": "Einbetten",
+ "multimediaviewer-download-link": "Diese Datei herunterladen",
+ "multimediaviewer-download-preview-link-title": "Im Browser ansehen",
+ "multimediaviewer-download-original-button-name": "Originaldatei herunterladen",
+ "multimediaviewer-download-small-button-name": "In kleiner Größe herunterladen",
+ "multimediaviewer-download-medium-button-name": "In mittlerer Größe herunterladen",
+ "multimediaviewer-download-large-button-name": "In großer Größe herunterladen",
+ "multimediaviewer-link-to-page": "Link zur Dateibeschreibungsseite",
+ "multimediaviewer-link-to-file": "Link zur Originaldatei",
+ "multimediaviewer-share-explanation": "Kopieren und den Link teilen",
+ "multimediaviewer-embed-wt": "Wikitext",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-embed-explanation": "Diesen Code zum Einbetten der Datei verwenden",
+ "multimediaviewer-text-embed-credit-text-bl": "Von $1, $2, $3",
+ "multimediaviewer-text-embed-credit-text-b": "Von $1, $2",
+ "multimediaviewer-html-embed-credit-text-bl": "Von $1, $2, $3",
+ "multimediaviewer-html-embed-credit-text-b": "Von $1, $2",
+ "multimediaviewer-html-embed-credit-link-text": "Link",
+ "multimediaviewer-embed-byline": "Von $1",
+ "multimediaviewer-embed-license": "Lizenziert unter $1.",
+ "multimediaviewer-embed-via": "Über $1.",
+ "multimediaviewer-default-embed-dimensions": "Standardvorschaubildgröße",
+ "multimediaviewer-original-embed-dimensions": "Originaldatei $1",
+ "multimediaviewer-large-embed-dimensions": "Groß $1",
+ "multimediaviewer-medium-embed-dimensions": "Mittel $1",
+ "multimediaviewer-small-embed-dimensions": "Klein $1",
+ "multimediaviewer-description-page-button-text": "Weitere Einzelheiten zu dieser Datei",
+ "multimediaviewer-description-page-popup-text": "Weitere Einzelheiten zu dieser Datei auf $1",
+ "multimediaviewer-commons-subtitle": "Das freie Medienarchiv",
+ "multimediaviewer-view-expanded": "Im Medienbetrachter öffnen",
+ "multimediaviewer-view-config": "Konfiguration",
+ "multimediaviewer-close-popup-text": "Dieses Werkzeug schließen (Esc)",
+ "multimediaviewer-fullscreen-popup-text": "Vollbildansicht aktivieren",
+ "multimediaviewer-defullscreen-popup-text": "Vollbildansicht deaktivieren",
+ "multimediaviewer-next-image-alt-text": "Nächstes Bild anzeigen",
+ "multimediaviewer-prev-image-alt-text": "Vorheriges Bild anzeigen",
+ "multimediaviewer-title-popup-text": "Beschreibung",
+ "multimediaviewer-credit-popup-text": "Informationen zu Urheber und Quelle",
+ "multimediaviewer-title-popup-text-more": "Vollständige Beschreibung anzeigen",
+ "multimediaviewer-credit-popup-text-more": "Vollständigen Urheber und Quelle anzeigen",
+ "multimediaviewer-download-attribution-cta-header": "Du musst den Urheber angeben",
+ "multimediaviewer-download-optional-attribution-cta-header": "Du kannst den Urheber angeben",
+ "multimediaviewer-download-attribution-cta": "Zeig mir wie",
+ "multimediaviewer-download-attribution-copy": "Wähle den Namensnennungstext für diese Datei aus und kopiere ihn (falls unterstützt)",
+ "multimediaviewer-reuse-warning-deletion": "Diese Datei wurde zur Löschung vorgeschlagen.",
+ "multimediaviewer-reuse-warning-nonfree": "Diese Datei hat keine freie Lizenz.",
+ "multimediaviewer-reuse-warning-noattribution": "Diese Datei hat keine Informationen zur Namensnennung.",
+ "multimediaviewer-reuse-warning-generic": "Überprüfe [$1 ihre Einzelheiten], bevor du sie verwendest.",
+ "multimediaviewer-attr-plain": "Klartext",
+ "multimediaviewer-options-tooltip": "Medienbetrachter aktivieren oder deaktivieren",
+ "multimediaviewer-options-dialog-header": "Medienbetrachter deaktivieren?",
+ "multimediaviewer-options-text-header": "Diese Betrachtungsfunktion für alle Dateien überspringen.",
+ "multimediaviewer-options-text-body": "Du kannst den Medienbetrachter später durch die Dateibeschreibungsseite aktivieren.",
+ "multimediaviewer-options-learn-more": "Weitere Informationen",
+ "multimediaviewer-option-submit-button": "Medienbetrachter deaktivieren",
+ "multimediaviewer-option-cancel-button": "Abbrechen",
+ "multimediaviewer-disable-confirmation-header": "Du hast den Medienbetrachter deaktiviert",
+ "multimediaviewer-disable-confirmation-text": "Wenn du das nächste Mal auf ein Vorschaubild auf $1 klickst, wirst du direkt alle Einzelheiten zur Datei ansehen können.",
+ "multimediaviewer-enable-dialog-header": "Medienbetrachter aktivieren?",
+ "multimediaviewer-enable-text-header": "Diese Medienbetrachtungsfunktion für alle Dateien standardmäßig aktivieren.",
+ "multimediaviewer-enable-submit-button": "Medienbetrachter aktivieren",
+ "multimediaviewer-enable-confirmation-header": "Du hast den Medienbetrachter für alle Dateien aktiviert",
+ "multimediaviewer-enable-confirmation-text": "Wenn du das nächste Mal auf ein Vorschaubild auf $1 klickst, wird der Medienbetrachter verwendet.",
+ "multimediaviewer-enable-alert": "Der Medienbetrachter ist jetzt deaktiviert",
+ "multimediaviewer-disable-info-title": "Du hast den Medienbetrachter deaktiviert",
+ "multimediaviewer-disable-info": "Du kannst dennoch einzelne Dateien mit dem Medienbetrachter ansehen.",
+ "multimediaviewer-errorreport-privacywarning": "Einzelheiten zum Fehler werden dem Bericht beigefügt, die öffentlich sichtbar sind. Falls du mit diesem Vorgehen nicht einverstanden bist, kannst du den Bericht unten bearbeiten und alle Daten entfernen, die du nicht teilen möchtest."
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/diq.json b/www/wiki/extensions/MultimediaViewer/i18n/diq.json
new file mode 100644
index 00000000..40bd8e7c
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/diq.json
@@ -0,0 +1,12 @@
+{
+ "@metadata": {
+ "authors": [
+ "Mirzali",
+ "Asmen",
+ "1917 Ekim Devrimi"
+ ]
+ },
+ "multimediaviewer-permission-viewmore": "Zêde bıvêne",
+ "multimediaviewer-discuss-mmv": "Werênayış",
+ "multimediaviewer-option-cancel-button": "Bıtexelne"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/dsb.json b/www/wiki/extensions/MultimediaViewer/i18n/dsb.json
new file mode 100644
index 00000000..b0c22b8a
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/dsb.json
@@ -0,0 +1,20 @@
+{
+ "@metadata": {
+ "authors": [
+ "Michawiki"
+ ]
+ },
+ "multimediaviewer-desc": "Miniaturki w pówjerchu połneje wobrazowki powětsyś.",
+ "multimediaviewer-pref": "Medijowy wobglědowak",
+ "multimediaviewer-pref-desc": "Polěpš swójo dožywjenje multimedijowego woglědowanja z toś tym rědom. Zwobraznja wobraze we wětšej wjelikosći na bokach, kótarež maju miniaturki. Wobraze pokazuju se w rědnjejšem pówjerchu połneje wobrazowki a daju se w połnej wjelikosći pśedstajiś.",
+ "multimediaviewer-file-page": "K pśisłušnemu datajowemu bokoju",
+ "multimediaviewer-repository-local": "Dalšne informacije",
+ "multimediaviewer-datetime-created": "Napórany $1",
+ "multimediaviewer-datetime-uploaded": "Nagraty $1",
+ "multimediaviewer-license-cc-pd": "Zjawnosći pśistupny",
+ "multimediaviewer-license-default": "Licencu se woglědaś",
+ "multimediaviewer-about-mmv": "Wó medijowem wobglědowaku",
+ "multimediaviewer-discuss-mmv": "Wo toś tej funkciji diskutěrowaś",
+ "multimediaviewer-geolocation": "Městno: $1",
+ "multimediaviewer-reuse-link": "Toś tu dataju wužywaś"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/el.json b/www/wiki/extensions/MultimediaViewer/i18n/el.json
new file mode 100644
index 00000000..783f1e81
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/el.json
@@ -0,0 +1,99 @@
+{
+ "@metadata": {
+ "authors": [
+ "Astralnet",
+ "Geraki",
+ "Nikosguard",
+ "Indoril",
+ "Evropi",
+ "Protnet",
+ "Xaris333",
+ "Glavkos",
+ "Nikosgranturismogt"
+ ]
+ },
+ "multimediaviewer-desc": "Επεκτείνετε τις μικρογραφίες σε μεγαλύτερο μέγεθος σε ένα περιβάλλον εργασίας πλήρους οθόνης.",
+ "multimediaviewer-pref": "Media Viewer",
+ "multimediaviewer-pref-desc": "Βελτιώστε την εμπειρία σας στην εμφάνιση πολυμέσων με αυτό το νέο εργαλείο. Εμφανίζει εικόνες σε μεγαλύτερο μέγεθος σε σελίδες που έχουν μικρογραφίες. Οι εικόνες εμφανίζονται σε μια καλύτερη πλήρους οθόνης διεπαφή επικάλυψης, και μπορούν επίσης να εμφανιστούν σε πλήρες μέγεθος.",
+ "multimediaviewer-optin-pref": "Ενεργοποίηση <span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About Media Viewer]</span>",
+ "multimediaviewer-file-page": "Μεταβείτε στην αντίστοιχη σελίδα του αρχείου",
+ "multimediaviewer-repository-local": "Περισσότερες λεπτομέρειες",
+ "multimediaviewer-datetime-created": "Δημιουργήθηκε: $1",
+ "multimediaviewer-datetime-uploaded": "Μεταφορτώθηκε:$1",
+ "multimediaviewer-metadata-error": "Σφάλμα: Δεν ήταν δυνατή η φόρτωση δεδομένων εικόνας. $1",
+ "multimediaviewer-thumbnail-error": "Σφάλμα: Δεν ήταν δυνατή η φόρτωση δεδομένων μικρογραφίας. $1",
+ "multimediaviewer-license-cc-by-4.0": "CC BY 4.0",
+ "multimediaviewer-license-cc-by-sa-4.0": "CC BY-SA 4.0",
+ "multimediaviewer-license-cc-pd": "Κοινό κτήμα",
+ "multimediaviewer-license-pd": "Κοινό Κτήμα",
+ "multimediaviewer-license-default": "Δείτε την άδεια",
+ "multimediaviewer-permission-title": "Λεπτομέρειες άδειας χρήσης",
+ "multimediaviewer-permission-link": "δείτε όρους",
+ "multimediaviewer-permission-viewmore": "Προβολή περισσοτέρων",
+ "multimediaviewer-about-mmv": "Σχετικά",
+ "multimediaviewer-discuss-mmv": "Συζήτηση",
+ "multimediaviewer-help-mmv": "Βοήθεια",
+ "multimediaviewer-optout-mmv": "Απενεργοποίηση Media Viewer",
+ "multimediaviewer-optin-mmv": "Ενεργοποίηση Media Viewer",
+ "multimediaviewer-optout-pending-mmv": "Απενεργοποίηση Media Viewer",
+ "multimediaviewer-optin-pending-mmv": "Ενεργοποίηση Media Viewer",
+ "multimediaviewer-optout-help": "Ο Media Viewer δεν θα χρησιμοποιείται πλέον για την εμφάνιση εικόνων. Για να το χρησιμοποιήσετε ξανά, κάντε κλικ στο «{{int:multimediaviewer-view-expanded}}» που βρίσκεται δίπλα σε κάθε εικόνα. Στη συνέχεια, κάντε κλικ στο «{{int:multimediaviewer-optin-mmv}}».",
+ "multimediaviewer-optin-help": "Το Media Viewer θα χρησιμοποιείται για την εμφάνιση εικόνων.",
+ "multimediaviewer-geolocation": "Τοποθεσία: $1",
+ "multimediaviewer-reuse-link": "Μοιραστείτε ή ενσωματώστε αυτό το αρχείο",
+ "multimediaviewer-reuse-loading-placeholder": "Φόρτωση σε εξέλιξη...",
+ "multimediaviewer-share-tab": "Κοινοποίηση",
+ "multimediaviewer-embed-tab": "Ενσωμάτωση",
+ "multimediaviewer-download-link": "Λήψη αυτού του αρχείου",
+ "multimediaviewer-download-preview-link-title": "Προβολή στον περιηγητή",
+ "multimediaviewer-download-original-button-name": "Κατέβασμα αρχικού αρχείου",
+ "multimediaviewer-download-small-button-name": "Λήψη μικρού μεγέθους",
+ "multimediaviewer-download-medium-button-name": "Λήψη μεσαίου μεγέθους",
+ "multimediaviewer-download-large-button-name": "Λήψη μεγάλου μεγέθους",
+ "multimediaviewer-link-to-page": "Σύνδεση με τη σελίδα περιγραφής αρχείου",
+ "multimediaviewer-link-to-file": "Σύνδεση με το αρχικό αρχείο",
+ "multimediaviewer-share-explanation": "Αντιγράψετε και μοιραστείτε ελεύθερα το σύνδεσμο",
+ "multimediaviewer-embed-wt": "Κώδικας wiki",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-embed-explanation": "Χρησιμοποιήσετε αυτόν τον κώδικα για να ενσωματώσετε το αρχείο",
+ "multimediaviewer-embed-byline": "Από $1",
+ "multimediaviewer-embed-license": "Υπό την άδεια $1.",
+ "multimediaviewer-embed-via": "Μέσω $1.",
+ "multimediaviewer-default-embed-dimensions": "Προεπιλεγμένο μέγεθος μικρογραφίας",
+ "multimediaviewer-original-embed-dimensions": "Πρωτότυπο αρχείο $1",
+ "multimediaviewer-large-embed-dimensions": "Μεγάλο $1",
+ "multimediaviewer-medium-embed-dimensions": "Μεσαίο $1",
+ "multimediaviewer-small-embed-dimensions": "Μικρό $1",
+ "multimediaviewer-description-page-button-text": "Περισσότερες λεπτομέρειες σχετικά με αυτό το αρχείο",
+ "multimediaviewer-description-page-popup-text": "Περισσότερες λεπτομέρειες σχετικά με αυτό το αρχείο στο $1",
+ "multimediaviewer-commons-subtitle": "Το ελεύθερο αποθετήριο πολυμέσων",
+ "multimediaviewer-view-expanded": "Άνοιγμα στο Media Viewer",
+ "multimediaviewer-view-config": "Διαμόρφωση",
+ "multimediaviewer-close-popup-text": "Κλείσιμο αυτού του εργαλείου (Esc)",
+ "multimediaviewer-fullscreen-popup-text": "Εμφάνιση σε πλήρη οθόνη",
+ "multimediaviewer-defullscreen-popup-text": "Έξοδος από πλήρη οθόνη",
+ "multimediaviewer-title-popup-text": "Περιγραφή",
+ "multimediaviewer-credit-popup-text": "Δημιουργός και πληροφορίες πηγής",
+ "multimediaviewer-title-popup-text-more": "Εμφάνιση πλήρους περιγραφής",
+ "multimediaviewer-credit-popup-text-more": "Πλήρης εμφάνιση συγγραφέα και πηγής",
+ "multimediaviewer-download-attribution-cta-header": "Θα πρέπει να αναφέρετε το δημιουργό",
+ "multimediaviewer-download-attribution-cta": "Δείξτε μου πώς",
+ "multimediaviewer-attr-plain": "Απλό",
+ "multimediaviewer-options-tooltip": "Ενεργοποίηση ή απενεργοποίηση του Media Viewer",
+ "multimediaviewer-options-dialog-header": "Απενεργοποίηση Media Viewer;",
+ "multimediaviewer-options-text-header": "Παραλείψτε αυτή τη δυνατότητα προβολής για όλα τα αρχεία.",
+ "multimediaviewer-options-text-body": "Μπορείτε να τον ενεργοποιήσετε αργότερα μέσω της σελίδας λεπτομερειών αρχείου.",
+ "multimediaviewer-options-learn-more": "Μάθετε περισσότερα",
+ "multimediaviewer-option-submit-button": "Απενεργοποίηση Media Viewer",
+ "multimediaviewer-option-cancel-button": "Ακύρωση",
+ "multimediaviewer-disable-confirmation-header": "Έχετε απενεργοποιήσει το Media Viewer",
+ "multimediaviewer-disable-confirmation-text": "Την επόμενη φορά που κάνετε κλικ σε μια μικρογραφία στο $1, θα μπορείτε άμεσα να προβάλετε όλες τις λεπτομέρειες του αρχείου.",
+ "multimediaviewer-enable-dialog-header": "Ενεργοποίηση Media Viewer;",
+ "multimediaviewer-enable-text-header": "Ενεργοποιήστε αυτή τη δυνατότητα προβολής πολυμέσων για όλα τα αρχεία από προεπιλογή.",
+ "multimediaviewer-enable-submit-button": "Ενεργοποίηση Media Viewer",
+ "multimediaviewer-enable-confirmation-header": "Έχετε ενεργοποιημένο το Media Viewer για όλα τα αρχεία",
+ "multimediaviewer-enable-confirmation-text": "Την επόμενη φορά που θα κάνετε κλικ σε μια μικρογραφία στο $1, το Media Viewer θα χρησιμοποιηθεί.",
+ "multimediaviewer-enable-alert": "Ο Media Viewer είναι απενεργοποιημένος τώρα",
+ "multimediaviewer-disable-info-title": "Έχετε απενεργοποιήσει το Media Viewer",
+ "multimediaviewer-disable-info": "Μπορείτε ακόμα να προβάλετε μεμονωμένα αρχεία με το Media Viewer."
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/eml.json b/www/wiki/extensions/MultimediaViewer/i18n/eml.json
new file mode 100644
index 00000000..875700a2
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/eml.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Gloria sah"
+ ]
+ },
+ "multimediaviewer-discuss-mmv": "Discusiòun"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/en-gb.json b/www/wiki/extensions/MultimediaViewer/i18n/en-gb.json
new file mode 100644
index 00000000..b6df0fb7
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/en-gb.json
@@ -0,0 +1,11 @@
+{
+ "@metadata": {
+ "authors": [
+ "Shirayuki",
+ "Caliburn"
+ ]
+ },
+ "multimediaviewer-license-default": "View licence",
+ "multimediaviewer-permission-title": "Permission details",
+ "multimediaviewer-embed-license": "Licenced under $1."
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/en.json b/www/wiki/extensions/MultimediaViewer/i18n/en.json
new file mode 100644
index 00000000..5f793f41
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/en.json
@@ -0,0 +1,156 @@
+{
+ "@metadata": {
+ "authors": [
+ "Mark Holmquist <mtraceur@member.fsf.org>"
+ ]
+ },
+ "multimediaviewer-desc": "Expand thumbnails in a larger size in a fullscreen interface",
+ "multimediaviewer-pref": "Media Viewer",
+ "multimediaviewer-pref-desc": "Improve your multimedia viewing experience with this new tool. It displays images in larger size on pages that have thumbnails. Images are shown in a nicer fullscreen interface overlay, and can also be viewed in full-size.",
+ "multimediaviewer-optin-pref": "Enable <span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About Media Viewer]</span>",
+ "multimediaviewer-file-page": "Go to corresponding file page",
+ "multimediaviewer-repository-local": "More details",
+ "multimediaviewer-datetime-created": "Created: $1",
+ "multimediaviewer-datetime-uploaded": "Uploaded: $1",
+ "multimediaviewer-credit": "$1 - $2",
+ "multimediaviewer-credit-fallback": "View author information",
+ "multimediaviewer-multiple-authors": "{{PLURAL:$1|one more author|$1 more authors}}",
+ "multimediaviewer-multiple-authors-combine": "$1 and $2",
+ "multimediaviewer-metadata-error": "Could not load image details (error: $1)",
+ "multimediaviewer-thumbnail-error": "Sorry, the file cannot be displayed",
+ "multimediaviewer-thumbnail-error-description": "There seems to be a technical issue. You can $1 or $3 if it persists. Error: $2",
+ "multimediaviewer-thumbnail-error-retry": "retry",
+ "multimediaviewer-thumbnail-error-report": "report the issue",
+ "multimediaviewer-report-issue-url": "https://phabricator.wikimedia.org/maniphest/task/create/?projects=PHID-PROJ-cabyqp5sf4hyvauln3sq&description=$1",
+ "multimediaviewer-license-cc-by-1.0": "CC BY 1.0",
+ "multimediaviewer-license-cc-sa-1.0": "CC SA 1.0",
+ "multimediaviewer-license-cc-by-sa-1.0": "CC BY-SA 1.0",
+ "multimediaviewer-license-cc-by-2.0": "CC BY 2.0",
+ "multimediaviewer-license-cc-by-sa-2.0": "CC BY-SA 2.0",
+ "multimediaviewer-license-cc-by-2.1": "CC BY 2.1",
+ "multimediaviewer-license-cc-by-sa-2.1": "CC BY-SA 2.1",
+ "multimediaviewer-license-cc-by-2.5": "CC BY 2.5",
+ "multimediaviewer-license-cc-by-sa-2.5": "CC BY-SA 2.5",
+ "multimediaviewer-license-cc-by-3.0": "CC BY 3.0",
+ "multimediaviewer-license-cc-by-sa-3.0": "CC BY-SA 3.0",
+ "multimediaviewer-license-cc-by-4.0": "CC BY 4.0",
+ "multimediaviewer-license-cc-by-sa-4.0": "CC BY-SA 4.0",
+ "multimediaviewer-license-cc-pd": "Public Domain",
+ "multimediaviewer-license-cc-zero": "CC 0",
+ "multimediaviewer-license-pd": "Public Domain",
+ "multimediaviewer-license-default": "View license",
+ "multimediaviewer-permission-title": "Permission details",
+ "multimediaviewer-permission-link": "view terms",
+ "multimediaviewer-permission-link-hide": "hide terms",
+ "multimediaviewer-permission-viewmore": "View more",
+ "multimediaviewer-restriction-2257": "This image contains sexually explicit content which may be subject to the Child Protection and Obscenity Enforcement Act in the United States.",
+ "multimediaviewer-restriction-aus-reserve": "This image was photographed in an Australian Commonwealth reserve and cannot be used for commercial gain without a permit.",
+ "multimediaviewer-restriction-communist": "This image contains Communist insignia which may be banned in some countries.",
+ "multimediaviewer-restriction-costume": "This image depicts costuming and may be subject to legal restrictions.",
+ "multimediaviewer-restriction-currency": "This image represents a depiction of a unit of currency and may be subject to legal restrictions.",
+ "multimediaviewer-restriction-design": "The design of the subject of this image may be copyrighted and subject to legal restrictions.",
+ "multimediaviewer-restriction-fan-art": "This image is a work of fan art, and re-use may be subject to legal restrictions.",
+ "multimediaviewer-restriction-ihl": "This image contains symbols restricted by International Humanitarian Law.",
+ "multimediaviewer-restriction-insignia": "This image contains official insignia which may be subject to legal restrictions.",
+ "multimediaviewer-restriction-ita-mibac": "This image reproduces a property belonging to Italian cultural heritage and is restricted by Italian law.",
+ "multimediaviewer-restriction-nazi": "This image contains Nazi or other fascist insignia which may be banned in some countries.",
+ "multimediaviewer-restriction-personality": "This image contains persons who may have rights that legally restrict certain re-uses of the image without consent.",
+ "multimediaviewer-restriction-trademarked": "This image contains content which may be subject to trademark laws.",
+ "multimediaviewer-restriction-default": "This image may be restricted by legal provisions outside of copyright law. See the file description page for details.",
+ "multimediaviewer-restriction-default-and-others": "This image may be further restricted by other legal provisions outside of copyright law. See the file description page for details.",
+ "multimediaviewer-about-mmv": "About",
+ "multimediaviewer-discuss-mmv": "Discussion",
+ "multimediaviewer-help-mmv": "Help",
+ "multimediaviewer-optout-mmv": "Disable Media Viewer",
+ "multimediaviewer-optin-mmv": "Enable Media Viewer",
+ "multimediaviewer-optout-pending-mmv": "Disabling Media Viewer",
+ "multimediaviewer-optin-pending-mmv": "Enabling Media Viewer",
+ "multimediaviewer-optout-help": "Media Viewer will no longer be used to show images. To use it again, click on the \"{{int:multimediaviewer-view-expanded}}\" button next to any image. Then click on \"{{int:multimediaviewer-optin-mmv}}\".",
+ "multimediaviewer-optin-help": "Media Viewer will be used to show images.",
+ "multimediaviewer-geoloc-north": "N",
+ "multimediaviewer-geoloc-east": "E",
+ "multimediaviewer-geoloc-south": "S",
+ "multimediaviewer-geoloc-west": "W",
+ "multimediaviewer-geoloc-coord": "$1° $2′ $3″ $4",
+ "multimediaviewer-geoloc-coords": "$1, $2",
+ "multimediaviewer-geolocation": "Location: $1",
+ "multimediaviewer-reuse-link": "Share or embed this file",
+ "multimediaviewer-reuse-loading-placeholder": "Loading…",
+ "multimediaviewer-reuse-copy-share": "Select and copy (if supported) the link for sharing this file",
+ "multimediaviewer-reuse-copy-embed": "Select and copy (if supported) the code for embedding this file",
+ "multimediaviewer-share-tab": "Share",
+ "multimediaviewer-embed-tab": "Embed",
+ "multimediaviewer-download-link": "Download this file",
+ "multimediaviewer-download-preview-link-title": "View in browser",
+ "multimediaviewer-download-original-button-name": "Download original file",
+ "multimediaviewer-download-small-button-name": "Download small size",
+ "multimediaviewer-download-medium-button-name": "Download medium size",
+ "multimediaviewer-download-large-button-name": "Download large size",
+ "multimediaviewer-link-to-page": "Link to file description page",
+ "multimediaviewer-link-to-file": "Link to original file",
+ "multimediaviewer-share-explanation": "Copy and freely share the link",
+ "multimediaviewer-embed-wt": "Wikitext",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-embed-explanation": "Use this code to embed the file",
+ "multimediaviewer-text-embed-credit-text-bl": "By $1, $2, $3",
+ "multimediaviewer-text-embed-credit-text-b": "By $1, $2",
+ "multimediaviewer-text-embed-credit-text-l": "$1, $2",
+ "multimediaviewer-html-embed-credit-text-bl": "By $1, $2, $3",
+ "multimediaviewer-html-embed-credit-text-b": "By $1, $2",
+ "multimediaviewer-html-embed-credit-text-l": "$1, $2",
+ "multimediaviewer-html-embed-credit-link-text": "Link",
+ "multimediaviewer-embed-byline": "By $1",
+ "multimediaviewer-embed-license": "Licensed under $1.",
+ "multimediaviewer-embed-license-nonfree": "$1.",
+ "multimediaviewer-embed-via": "Via $1.",
+ "multimediaviewer-default-embed-dimensions": "Default thumbnail size",
+ "multimediaviewer-original-embed-dimensions": "Original file $1",
+ "multimediaviewer-large-embed-dimensions": "Large $1",
+ "multimediaviewer-medium-embed-dimensions": "Medium $1",
+ "multimediaviewer-small-embed-dimensions": "Small $1",
+ "multimediaviewer-embed-dimensions": "$1 × $2 px",
+ "multimediaviewer-embed-dimensions-separated": "- $1",
+ "multimediaviewer-embed-dimensions-with-file-format": "$1 $2",
+ "multimediaviewer-description-page-button-text": "More details about this file",
+ "multimediaviewer-description-page-popup-text": "More details about this file on $1",
+ "multimediaviewer-commons-subtitle": "The free media repository",
+ "multimediaviewer-view-expanded": "Open in Media Viewer",
+ "multimediaviewer-view-config": "Configuration",
+ "multimediaviewer-close-popup-text": "Close this tool (Esc)",
+ "multimediaviewer-fullscreen-popup-text": "Show in full screen",
+ "multimediaviewer-defullscreen-popup-text": "Exit full screen",
+ "multimediaviewer-next-image-alt-text": "Show next image",
+ "multimediaviewer-prev-image-alt-text": "Show previous image",
+ "multimediaviewer-title-popup-text": "Description",
+ "multimediaviewer-credit-popup-text": "Author and source information",
+ "multimediaviewer-title-popup-text-more": "View full description",
+ "multimediaviewer-credit-popup-text-more": "View full author and source",
+ "multimediaviewer-download-attribution-cta-header": "You need to attribute the author",
+ "multimediaviewer-download-optional-attribution-cta-header": "You can attribute the author",
+ "multimediaviewer-download-attribution-cta": "Show me how",
+ "multimediaviewer-download-attribution-copy": "Select and copy (if supported) the attribution text for this file",
+ "multimediaviewer-reuse-warning-deletion": "This file is considered for deletion.",
+ "multimediaviewer-reuse-warning-nonfree": "This file does not have a free license.",
+ "multimediaviewer-reuse-warning-noattribution": "This file has no attribution information.",
+ "multimediaviewer-reuse-warning-generic": "Check [$1 its details] before using it.",
+ "multimediaviewer-attr-plain": "Plain",
+ "multimediaviewer-attr-html": "HTML",
+ "multimediaviewer-options-tooltip": "Enable or disable Media Viewer",
+ "multimediaviewer-options-dialog-header": "Disable Media Viewer?",
+ "multimediaviewer-options-text-header": "Skip this viewing feature for all files.",
+ "multimediaviewer-options-text-body": "You can enable it later through the file details page.",
+ "multimediaviewer-options-learn-more": "Learn more",
+ "multimediaviewer-option-submit-button": "Disable Media Viewer",
+ "multimediaviewer-option-cancel-button": "Cancel",
+ "multimediaviewer-disable-confirmation-header": "You have disabled Media Viewer",
+ "multimediaviewer-disable-confirmation-text": "Next time you click on a thumbnail on $1, you will directly view all file details.",
+ "multimediaviewer-enable-dialog-header": "Enable Media Viewer?",
+ "multimediaviewer-enable-text-header": "Enable this media viewing feature for all files by default.",
+ "multimediaviewer-enable-submit-button": "Enable Media Viewer",
+ "multimediaviewer-enable-confirmation-header": "You have enabled Media Viewer for all files",
+ "multimediaviewer-enable-confirmation-text": "Next time you click on a thumbnail on $1, Media Viewer will be used.",
+ "multimediaviewer-enable-alert": "Media Viewer is now disabled",
+ "multimediaviewer-disable-info-title": "You have disabled Media Viewer",
+ "multimediaviewer-disable-info": "You can still view individual files with Media Viewer.",
+ "multimediaviewer-errorreport-privacywarning": "Details of the error are attached to the report, which will be publicly viewable. If you are not comfortable with that, you can edit the report below and remove all the data you don't want to share."
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/eo.json b/www/wiki/extensions/MultimediaViewer/i18n/eo.json
new file mode 100644
index 00000000..a7541f92
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/eo.json
@@ -0,0 +1,103 @@
+{
+ "@metadata": {
+ "authors": [
+ "KuboF",
+ "Tlustulimu",
+ "Macofe",
+ "Robin van der Vliet"
+ ]
+ },
+ "multimediaviewer-desc": "Etendi miniaturojn al larĝa grandeco en plenekrana interfaco.",
+ "multimediaviewer-pref": "Media Montrilo",
+ "multimediaviewer-pref-desc": "Plibonigi vian spektadon de plurmediaĵoj per tiu ĉi nova ilo. En paĝoj kie estas uzataj miniaturoj, ĝi montras bildojn pli grande. Bildoj estas montrataj en pli bela plenekrana interfaco kaj eblas ilin spekti en plena grandeco.",
+ "multimediaviewer-optin-pref": "Ŝalti <span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About Median Montrilon]</span>",
+ "multimediaviewer-file-page": "Iri al la paĝo kun priskribo de la dosiero",
+ "multimediaviewer-repository-local": "Pliaj detaloj",
+ "multimediaviewer-datetime-created": "Kreita: $1",
+ "multimediaviewer-datetime-uploaded": "Alŝutita: $1",
+ "multimediaviewer-credit-fallback": "Montri informojn pri aŭtoro",
+ "multimediaviewer-multiple-authors": "{{PLURAL:$1|unu plia aŭtoro|$1 pliaj aŭtoroj}}",
+ "multimediaviewer-multiple-authors-combine": "$1 kaj $2",
+ "multimediaviewer-metadata-error": "Ne eblas ŝuti detalojn pri la bildo (eraro: $1)",
+ "multimediaviewer-thumbnail-error": "Pardonu, la dosiero ne povas esti montrita",
+ "multimediaviewer-thumbnail-error-description": "Tio ĉi ŝajnas esti teĥnika problemo. Vi povas $1 aŭ $3 se ĝi persistas. Eraro: $2",
+ "multimediaviewer-thumbnail-error-retry": "reprovi",
+ "multimediaviewer-thumbnail-error-report": "raporti la problemon",
+ "multimediaviewer-license-cc-pd": "Publika havaĵo",
+ "multimediaviewer-license-pd": "Publika havaĵo",
+ "multimediaviewer-license-default": "Montri permesilon",
+ "multimediaviewer-permission-title": "Detaloj pri permesilo",
+ "multimediaviewer-permission-link": "montri kondiĉojn",
+ "multimediaviewer-permission-link-hide": "kaŝi kondiĉojn",
+ "multimediaviewer-permission-viewmore": "Montri pli",
+ "multimediaviewer-restriction-communist": "Tiu ĉi bildo enhavas komunismajn insignojn, kiuj povas estis malpermesitaj en iuj landoj.",
+ "multimediaviewer-restriction-trademarked": "Enhavo de tiu ĉi bildo povas esti protektata per komercmarkaj leĝoj.",
+ "multimediaviewer-about-mmv": "Pri",
+ "multimediaviewer-discuss-mmv": "Diskuti tiun ĉi funkcion",
+ "multimediaviewer-help-mmv": "Helpo",
+ "multimediaviewer-optout-mmv": "Malŝalti Median Montrilon",
+ "multimediaviewer-optin-mmv": "Ŝalti Median Montrilon",
+ "multimediaviewer-optout-pending-mmv": "Media Montrilo malŝaltiĝas",
+ "multimediaviewer-optin-pending-mmv": "Media Montrilo ŝaltiĝas",
+ "multimediaviewer-optout-help": "Media Montrilo ne plu estos uzata por montri bildojn. Por denove ekuzi ĝin, klaku al la butono \"{{int:multimediaviewer-view-expanded}}\" tuj apud ajna bildo. Poste klaku al \"{{int:multimediaviewer-optin-mmv}}\".",
+ "multimediaviewer-optin-help": "Media Montrilo estos uzata por montrado bildojn",
+ "multimediaviewer-geolocation": "Loko: $1",
+ "multimediaviewer-reuse-link": "Kunhavigi aŭ enkonstrui tiun ĉi dosieron",
+ "multimediaviewer-reuse-loading-placeholder": "Ŝutado...",
+ "multimediaviewer-share-tab": "Kunhavigi",
+ "multimediaviewer-embed-tab": "Enkonstrui",
+ "multimediaviewer-download-link": "Elŝuti tiun ĉi dosieron",
+ "multimediaviewer-download-preview-link-title": "Malfermi en retumilo",
+ "multimediaviewer-download-original-button-name": "Elŝuti originan dosieron",
+ "multimediaviewer-download-small-button-name": "Elŝuti malgrandan dosieron",
+ "multimediaviewer-download-medium-button-name": "Elŝuti mezgrandan dosieron",
+ "multimediaviewer-download-large-button-name": "Elŝuti grandan dosieron",
+ "multimediaviewer-link-to-page": "Ligilo al paĝo kun priskribo de la dosiero",
+ "multimediaviewer-link-to-file": "Ligilo al origina dosiero",
+ "multimediaviewer-share-explanation": "Kopiu kaj libere kunhavigu la ligilon",
+ "multimediaviewer-embed-wt": "Vikiteksto",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-embed-explanation": "Uzu tiun ĉi kodon por enkonstrui la dosieron",
+ "multimediaviewer-html-embed-credit-link-text": "Ligilo",
+ "multimediaviewer-embed-byline": "De $1",
+ "multimediaviewer-embed-license": "Licencita laŭ $1.",
+ "multimediaviewer-embed-via": "De $1.",
+ "multimediaviewer-default-embed-dimensions": "Defaŭlta grandeco de miniaturo",
+ "multimediaviewer-original-embed-dimensions": "Origina dosiero $1",
+ "multimediaviewer-large-embed-dimensions": "Granda $1",
+ "multimediaviewer-medium-embed-dimensions": "Mezgranda $1",
+ "multimediaviewer-small-embed-dimensions": "Malgranda $1",
+ "multimediaviewer-description-page-button-text": "Pliaj detaloj pri tiu ĉi dosiero",
+ "multimediaviewer-description-page-popup-text": "Pliaj detaloj pri tiu ĉi dosiero en $1",
+ "multimediaviewer-commons-subtitle": "Libera doser-deponejo",
+ "multimediaviewer-view-expanded": "Malfermi en Media Montrilo",
+ "multimediaviewer-view-config": "Agordoj",
+ "multimediaviewer-close-popup-text": "Fermi tiun ĉi ilon (Esk)",
+ "multimediaviewer-fullscreen-popup-text": "Montri plenekrane",
+ "multimediaviewer-defullscreen-popup-text": "Eliri plenekranon",
+ "multimediaviewer-title-popup-text": "Priskribo",
+ "multimediaviewer-credit-popup-text": "Informoj pri aŭtoro kaj fonto",
+ "multimediaviewer-title-popup-text-more": "Montri plenan priskribon",
+ "multimediaviewer-credit-popup-text-more": "Montri plenajn informojn pri aŭtoro kaj fonto",
+ "multimediaviewer-download-attribution-cta-header": "Vi devas atribui la aŭtoron",
+ "multimediaviewer-download-optional-attribution-cta-header": "Vi povas atribui la aŭtoron",
+ "multimediaviewer-download-attribution-cta": "Montru al mi kiel",
+ "multimediaviewer-attr-plain": "Plata teksto",
+ "multimediaviewer-options-tooltip": "Ŝalti aŭ malŝalti Median Montrilon",
+ "multimediaviewer-options-dialog-header": "Ĉu malŝalti Median Montrilon?",
+ "multimediaviewer-options-text-header": "Tio malŝaltos tiun ĉi montro-funkcion por ĉiuj dosieroj.",
+ "multimediaviewer-options-text-body": "Vi povas ĝin denove ŝalti en la paĝo kun detaloj pri dosiero.",
+ "multimediaviewer-options-learn-more": "Lernu pli",
+ "multimediaviewer-option-submit-button": "Malŝalti Median Montrilon",
+ "multimediaviewer-option-cancel-button": "Nuligi",
+ "multimediaviewer-disable-confirmation-header": "Vi malŝaltis Median Montrilon",
+ "multimediaviewer-disable-confirmation-text": "Sekvafoje kiam vi klakos al miniaturo en $1, vi rekte vidos ĉiujn dosierajn detalojn.",
+ "multimediaviewer-enable-dialog-header": "Ĉu ŝalti Median Montrilon?",
+ "multimediaviewer-enable-text-header": "Ŝalti tiun ĉi medi-montran funkcion defaŭlte por ĉiuj dosieroj.",
+ "multimediaviewer-enable-submit-button": "Ŝalti Median Montrilon",
+ "multimediaviewer-enable-confirmation-header": "Vi ŝaltis Median Montrilon por ĉiuj dosieroj",
+ "multimediaviewer-enable-confirmation-text": "Sekvafoje kiam vi klakos al miniaturo en $1, estos uzata Media Montrilo.",
+ "multimediaviewer-enable-alert": "Media Montrilo nun estas malŝaltita.",
+ "multimediaviewer-disable-info-title": "Vi malŝaltis Median Montrilon",
+ "multimediaviewer-disable-info": "Vi daŭre povas vidigi unuopajn dosierojn per Media Montrilo."
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/es-formal.json b/www/wiki/extensions/MultimediaViewer/i18n/es-formal.json
new file mode 100644
index 00000000..91b45a6f
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/es-formal.json
@@ -0,0 +1,57 @@
+{
+ "@metadata": {
+ "authors": [
+ "DJ Nietzsche",
+ "Jduranboger",
+ "Baffo"
+ ]
+ },
+ "multimediaviewer-desc": "Expande las miniaturas a un tamaño mayor en modo de pantalla completa.",
+ "multimediaviewer-pref": "Visor Multimedia",
+ "multimediaviewer-pref-desc": "Mejora tu experienecia de visualización multimedia con ésta nueva herramienta. El Visor Multimedia muestra las imágenes más grandes en las páginas que tienen miniaturas. Las imágenes se muestran en una interfaz de pantalla completa más agradable, y también pueden verse a tamaño completo.",
+ "multimediaviewer-optin-pref": "Habilita nueva experiencia de visualización multimedia",
+ "multimediaviewer-file-page": "Ir a la página del archivo",
+ "multimediaviewer-repository-local": "Aprende más",
+ "multimediaviewer-datetime-created": "Creado en $1",
+ "multimediaviewer-datetime-uploaded": "Subido en $1",
+ "multimediaviewer-metadata-error": "Error: No se puede cargar datos de la imagen. $1",
+ "multimediaviewer-thumbnail-error": "Error: No se puede cargar datos de la miniatura. $1",
+ "multimediaviewer-license-cc-pd": "Dominio Público",
+ "multimediaviewer-license-pd": "Dominio Público",
+ "multimediaviewer-license-default": "Ver licencia",
+ "multimediaviewer-permission-title": "Detalles de la licencia",
+ "multimediaviewer-permission-link": "ver términos",
+ "multimediaviewer-permission-viewmore": "Ver más",
+ "multimediaviewer-about-mmv": "Acerca del Visor Multimedia",
+ "multimediaviewer-discuss-mmv": "Dejar comentarios",
+ "multimediaviewer-help-mmv": "Ayuda",
+ "multimediaviewer-geolocation": "Ubicación: $1",
+ "multimediaviewer-reuse-link": "Compartir o Insertar este archivo",
+ "multimediaviewer-reuse-loading-placeholder": "Cargando...",
+ "multimediaviewer-share-tab": "Compartir",
+ "multimediaviewer-embed-tab": "Incrustar",
+ "multimediaviewer-download-link": "Descargar",
+ "multimediaviewer-download-preview-link-title": "Previsualizar en navegador",
+ "multimediaviewer-download-original-button-name": "Descargar tamaño original",
+ "multimediaviewer-download-small-button-name": "Descargar tamaño pequeño",
+ "multimediaviewer-download-medium-button-name": "Descargar tamaño mediano",
+ "multimediaviewer-download-large-button-name": "Descargar tamaño grande",
+ "multimediaviewer-link-to-page": "Enlace a la página de descripción del archivo",
+ "multimediaviewer-link-to-file": "Enlace al archivo original",
+ "multimediaviewer-share-explanation": "Copiar y compartir el enlace",
+ "multimediaviewer-embed-wt": "Wikitexto",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-embed-explanation": "Utiliza este código para incrustar el archivo",
+ "multimediaviewer-embed-byline": "Por $1",
+ "multimediaviewer-embed-license": "Disponible bajo licencia $1.",
+ "multimediaviewer-embed-via": "Via $1.",
+ "multimediaviewer-default-embed-dimensions": "Tamaño de miniaturas predeterminado",
+ "multimediaviewer-original-embed-dimensions": "Tamaño original $1",
+ "multimediaviewer-large-embed-dimensions": "Grande $1",
+ "multimediaviewer-medium-embed-dimensions": "Mediano $1",
+ "multimediaviewer-small-embed-dimensions": "Pequeño $1",
+ "multimediaviewer-description-page-button-text": "Más detalles",
+ "multimediaviewer-description-page-popup-text": "Más detalles en $1",
+ "multimediaviewer-commons-subtitle": "El repositorio multimedia libre",
+ "multimediaviewer-view-expanded": "Vista expandida"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/es.json b/www/wiki/extensions/MultimediaViewer/i18n/es.json
new file mode 100644
index 00000000..8b948e7d
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/es.json
@@ -0,0 +1,138 @@
+{
+ "@metadata": {
+ "authors": [
+ "Benfutbol10",
+ "Carlitosag",
+ "Ciencia Al Poder",
+ "Csbotero",
+ "Fitoschido",
+ "PoLuX124",
+ "DJ Nietzsche",
+ "Jduranboger",
+ "Macofe",
+ "Themasterriot",
+ "Dgstranz"
+ ]
+ },
+ "multimediaviewer-desc": "Expande las miniaturas a un tamaño mayor en una vista de pantalla completa.",
+ "multimediaviewer-pref": "Visor multimedia",
+ "multimediaviewer-pref-desc": "Mejora tu experiencia de visualización multimedia con esta herramienta. Las imágenes se muestran en una vista a pantalla completa que incluye información relevante de las mismas.",
+ "multimediaviewer-optin-pref": "Activar el <span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About visor multimedia]</span>",
+ "multimediaviewer-file-page": "Ir a la página del archivo correspondiente",
+ "multimediaviewer-repository-local": "Más detalles",
+ "multimediaviewer-datetime-created": "Creado el: $1",
+ "multimediaviewer-datetime-uploaded": "Subido el: $1",
+ "multimediaviewer-credit-fallback": "Ver información del autor",
+ "multimediaviewer-multiple-authors": "{{PLURAL:$1|un autor más|$1 autores más}}",
+ "multimediaviewer-multiple-authors-combine": "$1 y $2",
+ "multimediaviewer-metadata-error": "No se pueden cargar detalles de la imagen (error: $1)",
+ "multimediaviewer-thumbnail-error": "No se puede mostrar el archivo",
+ "multimediaviewer-thumbnail-error-description": "Parece que hay un problema técnico. Puedes $1 o $3 si persiste. Error: $2",
+ "multimediaviewer-thumbnail-error-retry": "reintentar",
+ "multimediaviewer-thumbnail-error-report": "reportar el problema",
+ "multimediaviewer-license-cc-pd": "Dominio público",
+ "multimediaviewer-license-pd": "Dominio público",
+ "multimediaviewer-license-default": "Ver licencia",
+ "multimediaviewer-permission-title": "Detalles de permisos",
+ "multimediaviewer-permission-link": "ver términos",
+ "multimediaviewer-permission-link-hide": "ocultar términos",
+ "multimediaviewer-permission-viewmore": "Ver más",
+ "multimediaviewer-restriction-2257": "Esta imagen contiene contenido sexual explícito, el cual podría someterse a las estipulaciones de la Ley de Protección Infantil y Material Obsceno de los Estados Unidos.",
+ "multimediaviewer-restriction-aus-reserve": "Esta fotografía fue capturada en una reserva federal australiana y no puede utilizarse con fines comerciales sin previa autorización.",
+ "multimediaviewer-restriction-communist": "Esta imagen contiene simbología comunista que puede estar prohibida en algunos países.",
+ "multimediaviewer-restriction-costume": "Esta imagen muestra vestuarios y puede estar sujeta a restricciones legales.",
+ "multimediaviewer-restriction-currency": "Esta imagen constituye una representación de una unidad monetaria y puede estar sujeta a restricciones legales.",
+ "multimediaviewer-restriction-design": "El diseño del objeto representado en esta imagen puede tener derechos de autor y estar sujeta a restricciones legales.",
+ "multimediaviewer-restriction-fan-art": "Esta imagen es una obra de arte creado por fanáticos cuya reutilización puede estar sujeta a restricciones legales.",
+ "multimediaviewer-restriction-ihl": "Esta imagen contiene símbolos restringidos por el derecho internacional humanitario.",
+ "multimediaviewer-restriction-insignia": "Esta imagen contiene simbología oficial que puede estar sujeta a restricciones legales.",
+ "multimediaviewer-restriction-ita-mibac": "Esta imagen reproduce una propiedad del patrimonio cultural italiano y está restringida por las leyes italianas.",
+ "multimediaviewer-restriction-nazi": "Esta imagen contiene simbología nazi o fascista que puede estar prohibida en algunos países.",
+ "multimediaviewer-restriction-personality": "Esta imagen contiene personas que pueden tener derechos que restrinjan legalmente la reutilización sin consentimiento de esta imagen.",
+ "multimediaviewer-restriction-trademarked": "Esta imagen incluye contenido que puede estar sujeto a leyes sobre registro de marcas.",
+ "multimediaviewer-restriction-default": "Esta imagen puede estar restringida por disposiciones legales no relacionadas con la propiedad intelectual. Consulta la página de descripción para obtener detalles.",
+ "multimediaviewer-restriction-default-and-others": "Esta imagen puede estar aún restringida por disposiciones legales no relacionadas con la propiedad intelectual. Consulta la página de descripción del archivo para obtener detalles.",
+ "multimediaviewer-about-mmv": "Acerca de",
+ "multimediaviewer-discuss-mmv": "Discusión",
+ "multimediaviewer-help-mmv": "Ayuda",
+ "multimediaviewer-optout-mmv": "Desactivar el visor multimedia",
+ "multimediaviewer-optin-mmv": "Activar el visor multimedia",
+ "multimediaviewer-optout-pending-mmv": "Desactivando el visor multimedia",
+ "multimediaviewer-optin-pending-mmv": "Activando el visor multimedia",
+ "multimediaviewer-optout-help": "El Visor multimedia ya no se usará para mostrar imágenes. Para volver a usarlo, haz clic en el botón «{{int:multimediaviewer-view-expanded}}» junto a cada imagen. Luego haz clic en «{{int:multimediaviewer-optin-mmv}}».",
+ "multimediaviewer-optin-help": "Las imágenes se abrirán ahora con el visor multimedia.",
+ "multimediaviewer-geolocation": "Ubicación: $1",
+ "multimediaviewer-reuse-link": "Compartir o incrustar este archivo",
+ "multimediaviewer-reuse-loading-placeholder": "Cargando…",
+ "multimediaviewer-reuse-copy-share": "Selecciona y copia (si se permite) el enlace para compartir este archivo",
+ "multimediaviewer-reuse-copy-embed": "Selecciona y copia (si se permite) el código para incrustar este archivo",
+ "multimediaviewer-share-tab": "Compartir",
+ "multimediaviewer-embed-tab": "Incrustar",
+ "multimediaviewer-download-link": "Descargar este archivo",
+ "multimediaviewer-download-preview-link-title": "Ver en el navegador",
+ "multimediaviewer-download-original-button-name": "Descargar el archivo original",
+ "multimediaviewer-download-small-button-name": "Descargar tamaño pequeño",
+ "multimediaviewer-download-medium-button-name": "Descargar tamaño mediano",
+ "multimediaviewer-download-large-button-name": "Descargar tamaño grande",
+ "multimediaviewer-link-to-page": "Enlace a la página de descripción del archivo",
+ "multimediaviewer-link-to-file": "Enlace al archivo original",
+ "multimediaviewer-share-explanation": "Copiar y compartir el enlace",
+ "multimediaviewer-embed-wt": "Wikitexto",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-embed-explanation": "Utiliza este código para incrustar el archivo",
+ "multimediaviewer-text-embed-credit-text-bl": "De $1, $2, $3",
+ "multimediaviewer-text-embed-credit-text-b": "De $1, $2",
+ "multimediaviewer-html-embed-credit-text-bl": "De $1, $2, $3",
+ "multimediaviewer-html-embed-credit-text-b": "De $1, $2",
+ "multimediaviewer-html-embed-credit-link-text": "Enlace",
+ "multimediaviewer-embed-byline": "Por $1",
+ "multimediaviewer-embed-license": "Disponible bajo la licencia $1.",
+ "multimediaviewer-embed-via": "Vía $1.",
+ "multimediaviewer-default-embed-dimensions": "Tamaño de miniaturas predeterminado",
+ "multimediaviewer-original-embed-dimensions": "Archivo original, $1",
+ "multimediaviewer-large-embed-dimensions": "Grande, $1",
+ "multimediaviewer-medium-embed-dimensions": "Mediano, $1",
+ "multimediaviewer-small-embed-dimensions": "Pequeño, $1",
+ "multimediaviewer-embed-dimensions": "$1 × $2 px",
+ "multimediaviewer-description-page-button-text": "Más detalles sobre este archivo",
+ "multimediaviewer-description-page-popup-text": "Más detalles sobre este archivo en $1",
+ "multimediaviewer-commons-subtitle": "El repositorio de multimedia libre",
+ "multimediaviewer-view-expanded": "Abrir en el visor multimedia",
+ "multimediaviewer-view-config": "Configuración",
+ "multimediaviewer-close-popup-text": "Cerrar esta herramienta (Esc)",
+ "multimediaviewer-fullscreen-popup-text": "Mostrar a pantalla completa",
+ "multimediaviewer-defullscreen-popup-text": "Salir de pantalla completa",
+ "multimediaviewer-next-image-alt-text": "Mostrar imagen siguiente",
+ "multimediaviewer-prev-image-alt-text": "Mostrar imagen anterior",
+ "multimediaviewer-title-popup-text": "Descripción",
+ "multimediaviewer-credit-popup-text": "Información de autor y fuente",
+ "multimediaviewer-title-popup-text-more": "Ver descripción completa",
+ "multimediaviewer-credit-popup-text-more": "Ver la fuente y el autor completos",
+ "multimediaviewer-download-attribution-cta-header": "Es necesario dar reconocimiento al autor",
+ "multimediaviewer-download-optional-attribution-cta-header": "Puedes dar reconocimiento al autor",
+ "multimediaviewer-download-attribution-cta": "Mostrarme cómo",
+ "multimediaviewer-download-attribution-copy": "Selecciona y copia (si es posible) el texto de atribución de este archivo",
+ "multimediaviewer-reuse-warning-deletion": "Se está considerando el borrado de este archivo.",
+ "multimediaviewer-reuse-warning-nonfree": "Este archivo no está disponible bajo una licencia libre.",
+ "multimediaviewer-reuse-warning-noattribution": "Este archivo no tiene información de atribución.",
+ "multimediaviewer-reuse-warning-generic": "Comprueba [$1 sus detalles] antes de utilizarlo.",
+ "multimediaviewer-attr-plain": "Texto sin formato",
+ "multimediaviewer-options-tooltip": "Activar o desactivar el Visor multimedia",
+ "multimediaviewer-options-dialog-header": "¿Desactivar el visor multimedia?",
+ "multimediaviewer-options-text-header": "Omitir esta función de visualización para todos los archivos.",
+ "multimediaviewer-options-text-body": "Puedes activarlo más tarde mediante la página de detalles de archivos.",
+ "multimediaviewer-options-learn-more": "Más información",
+ "multimediaviewer-option-submit-button": "Desactivar el visor multimedia",
+ "multimediaviewer-option-cancel-button": "Cancelar",
+ "multimediaviewer-disable-confirmation-header": "Has desactivado el visor multimedia",
+ "multimediaviewer-disable-confirmation-text": "La próxima vez que haga clic en una miniatura en $1, verá de manera directa los detalles del archivo.",
+ "multimediaviewer-enable-dialog-header": "¿Activar el visor multimedia?",
+ "multimediaviewer-enable-text-header": "Activar esta función de visualización para todos los archivos de manera predeterminada.",
+ "multimediaviewer-enable-submit-button": "Activar el visor multimedia",
+ "multimediaviewer-enable-confirmation-header": "Has activado el visor multimedia para todos los archivos",
+ "multimediaviewer-enable-confirmation-text": "La próxima vez que hagas clic sobre una miniatura en $1, se usará el visor multimedia.",
+ "multimediaviewer-enable-alert": "Se ha desactivo el visor multimedia",
+ "multimediaviewer-disable-info-title": "Has desactivado el visor multimedia",
+ "multimediaviewer-disable-info": "Aún puede ver archivos individuales con el Visor Multimedia.",
+ "multimediaviewer-errorreport-privacywarning": "El informe contiene detalles del error que serán visibles públicamente. Si ello no te inspira confianza, puedes modificar el informe para quitarle cualquier dato que no quieras compartir."
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/et.json b/www/wiki/extensions/MultimediaViewer/i18n/et.json
new file mode 100644
index 00000000..e34b6858
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/et.json
@@ -0,0 +1,116 @@
+{
+ "@metadata": {
+ "authors": [
+ "Pikne",
+ "WikedKentaur",
+ "Metsavend",
+ "Macofe"
+ ]
+ },
+ "multimediaviewer-desc": "Võimaldab vaadata pisipilte täisekraaniliideses.",
+ "multimediaviewer-pref": "Failivaatur",
+ "multimediaviewer-pref-desc": "Täienda meediafailide vaatamise võimalusi selle uue tööriistaga. See võimaldab kuvada pisipiltidega lehekülgedel pildid suuremana. Pilte saab näidata kenamas täisekraaniliideses ja ka täissuuruses.",
+ "multimediaviewer-optin-pref": "Kasuta <span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About failivaaturit]</span>",
+ "multimediaviewer-file-page": "Mine failileheküljele",
+ "multimediaviewer-repository-local": "Rohkem üksikasju",
+ "multimediaviewer-datetime-created": "Valmistatud: $1",
+ "multimediaviewer-datetime-uploaded": "Üles laaditud: $1",
+ "multimediaviewer-credit": "$1 – $2",
+ "multimediaviewer-credit-fallback": "Vaata autoriteavet",
+ "multimediaviewer-multiple-authors": "{{PLURAL:$1|veel üks autor|veel $1 autorit}}",
+ "multimediaviewer-multiple-authors-combine": "$1 ja $2",
+ "multimediaviewer-metadata-error": "Pildi üksikasju ei õnnestunud laadida (tõrge: $1)",
+ "multimediaviewer-thumbnail-error": "Failinime kahjuks ei õnnestu kuvada",
+ "multimediaviewer-thumbnail-error-description": "Paistab, et tegu on tehnilise probleemiga. Saad $1 või $3, kui see on püsiv. Tõrge: $2",
+ "multimediaviewer-thumbnail-error-retry": "uuesti proovida",
+ "multimediaviewer-thumbnail-error-report": "probleemist teatada",
+ "multimediaviewer-license-cc-pd": "Avalik omand",
+ "multimediaviewer-license-pd": "Avalik omand",
+ "multimediaviewer-license-default": "Vaata litsentsi",
+ "multimediaviewer-permission-title": "Loa üksikasjad",
+ "multimediaviewer-permission-link": "vaata tingimusi",
+ "multimediaviewer-permission-link-hide": "peida tingimused",
+ "multimediaviewer-permission-viewmore": "Vaata rohkem",
+ "multimediaviewer-restriction-insignia": "See pilt sisaldab ametlikku märki, mille suhtes võivad kohalduda õiguslikud piirangud.",
+ "multimediaviewer-restriction-personality": "Sellel pildil on kujutatud isikuid, kellel võivad olla õigused, mis piiravad õiguslikult pildi nõusolekuta edasikasutust.",
+ "multimediaviewer-restriction-trademarked": "Sellel pildil on elemente, mille suhtes võidakse kohaldada kaubamärgiseadusi.",
+ "multimediaviewer-restriction-default": "Selle pildi kasutus võib olla piiratud õiguslike sätetega, mis ei tulene autoriõiguse seadusest. Üksikasjade kohta vaata pildi kirjeldusleheküljelt.",
+ "multimediaviewer-restriction-default-and-others": "Selle pildi kasutust võivad piirata ka õiguslikud sätted, mis ei tulene autoriõiguse seadusest. Üksikasjade kohta vaata pildi kirjeldusleheküljelt.",
+ "multimediaviewer-about-mmv": "Teave",
+ "multimediaviewer-discuss-mmv": "arutelu",
+ "multimediaviewer-help-mmv": "abi",
+ "multimediaviewer-optout-mmv": "Keela failivaatur",
+ "multimediaviewer-optin-mmv": "Luba failivaatur",
+ "multimediaviewer-optout-pending-mmv": "Failivaaturi keelamine...",
+ "multimediaviewer-optin-pending-mmv": "Failivaaturi lubamine...",
+ "multimediaviewer-optout-help": "Piltide näitamiseks ei kasutata enam failivaaturit. Et seda uuesti kasutada, klõpsa ükskõik millise pildi juures nuppu \"{{int:Multimediaviewer-view-expanded}}\". Seejärel klõpsa \"{{int:Multimediaviewer-optin-mmv}}\".",
+ "multimediaviewer-optin-help": "Piltide näitamiseks kasutatakse edaspidi failivaaturit.",
+ "multimediaviewer-geolocation": "Asukoht: $1",
+ "multimediaviewer-reuse-link": "Jaga või kasuta seda faili",
+ "multimediaviewer-reuse-loading-placeholder": "Laadimine...",
+ "multimediaviewer-reuse-copy-share": "Vali ja kopeeri link, et faili jagada (ei tööta mõnes brauseris)",
+ "multimediaviewer-reuse-copy-embed": "Vali ja kopeeri kood, et fail manustada (ei tööta mõnes brauseris)",
+ "multimediaviewer-share-tab": "Jagamine",
+ "multimediaviewer-embed-tab": "Manustamine",
+ "multimediaviewer-download-link": "Allalaadimine",
+ "multimediaviewer-download-preview-link-title": "Vaata brauseris",
+ "multimediaviewer-download-original-button-name": "Laadi alla algsuuruses",
+ "multimediaviewer-download-small-button-name": "Laadi alla väike pilt",
+ "multimediaviewer-download-medium-button-name": "Laadi alla keskmises suuruses",
+ "multimediaviewer-download-large-button-name": "Laadi alla suur pilt",
+ "multimediaviewer-link-to-page": "Link faili kirjelduslehele",
+ "multimediaviewer-link-to-file": "Link algse faili juurde",
+ "multimediaviewer-share-explanation": "Kopeeri ja jaga vabalt seda linki.",
+ "multimediaviewer-embed-wt": "Vikitekst",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-embed-explanation": "Kasuta seda koodi, et fail manustada.",
+ "multimediaviewer-text-embed-credit-text-bl": "$1, $2, $3",
+ "multimediaviewer-text-embed-credit-text-b": "$1, $2",
+ "multimediaviewer-html-embed-credit-text-bl": "$1, $2, $3",
+ "multimediaviewer-html-embed-credit-text-b": "$1, $2",
+ "multimediaviewer-embed-byline": "Autor: $1",
+ "multimediaviewer-embed-license": "Avaldatud litsentsi $1 all.",
+ "multimediaviewer-embed-via": "Avaldatud saidil $1.",
+ "multimediaviewer-default-embed-dimensions": "Pisipildi vaikesuurus",
+ "multimediaviewer-original-embed-dimensions": "Algsuurus $1",
+ "multimediaviewer-large-embed-dimensions": "Suur $1",
+ "multimediaviewer-medium-embed-dimensions": "Keskmine $1",
+ "multimediaviewer-small-embed-dimensions": "Väike $1",
+ "multimediaviewer-embed-dimensions-separated": "– $1",
+ "multimediaviewer-description-page-button-text": "Veel üksikasju selle faili kohta",
+ "multimediaviewer-description-page-popup-text": "Veel üksikasju selle faili kohta asukohas $1",
+ "multimediaviewer-commons-subtitle": "Vabade meediafailide varamu",
+ "multimediaviewer-view-expanded": "Ava failivaaturis",
+ "multimediaviewer-view-config": "Häälestus",
+ "multimediaviewer-close-popup-text": "Sule see tööriist (Esc)",
+ "multimediaviewer-fullscreen-popup-text": "Näita täisekraanil",
+ "multimediaviewer-defullscreen-popup-text": "Sule täisekraan",
+ "multimediaviewer-next-image-alt-text": "Näita järgmist pilti",
+ "multimediaviewer-prev-image-alt-text": "Näita eelmist pilti",
+ "multimediaviewer-title-popup-text": "Kirjeldus",
+ "multimediaviewer-credit-popup-text": "Autori- ja allikateave",
+ "multimediaviewer-title-popup-text-more": "Vaata kogu kirjeldust",
+ "multimediaviewer-credit-popup-text-more": "Vaata kogu autori- ja allikateavet",
+ "multimediaviewer-download-attribution-cta-header": "Sul tuleb viidata autorile",
+ "multimediaviewer-download-optional-attribution-cta-header": "Saad autorile viidata",
+ "multimediaviewer-download-attribution-cta": "Näita, kuidas",
+ "multimediaviewer-download-attribution-copy": "Vali ja kopeeri tekst, millega fail autorile omistada (ei tööta mõnes brauseris)",
+ "multimediaviewer-attr-plain": "Lihttekst",
+ "multimediaviewer-options-tooltip": "Luba või keela failivaatur",
+ "multimediaviewer-options-dialog-header": "Kas keelad failivaaturi?",
+ "multimediaviewer-options-text-header": "Seda vaaturit ei kasutata siis ühegi faili juures.",
+ "multimediaviewer-options-text-body": "Saad faili üksikasjade leheküljel selle hiljem lubada.",
+ "multimediaviewer-options-learn-more": "Lisateave",
+ "multimediaviewer-option-submit-button": "Keela failivaatur",
+ "multimediaviewer-option-cancel-button": "Loobu",
+ "multimediaviewer-disable-confirmation-header": "Oled failivaaturi keelanud",
+ "multimediaviewer-disable-confirmation-text": "Järgmine kord, kui võrgukohas $1 pisipildil klõpsad, näed kohe kõiki faili üksikasju.",
+ "multimediaviewer-enable-dialog-header": "Kas lubad failivaaturi?",
+ "multimediaviewer-enable-text-header": "Saad kõigi failide juures vaikimisi seda vaaturit kasutada.",
+ "multimediaviewer-enable-submit-button": "Luba failivaatur",
+ "multimediaviewer-enable-confirmation-header": "Oled failivaaturi kõigi failide jaoks sisse lülitanud.",
+ "multimediaviewer-enable-confirmation-text": "Järgmine kord, kui võrgukohas $1 pisipildil klõpsad, kasutatakse failivaaturit.",
+ "multimediaviewer-enable-alert": "Failivaatur on praegu keelatud",
+ "multimediaviewer-disable-info-title": "Oled failivaaturi keelanud",
+ "multimediaviewer-disable-info": "Saad endiselt iga üksikut pilti failivaaturiga vaadata."
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/eu.json b/www/wiki/extensions/MultimediaViewer/i18n/eu.json
new file mode 100644
index 00000000..e5efdf42
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/eu.json
@@ -0,0 +1,28 @@
+{
+ "@metadata": {
+ "authors": [
+ "Subi",
+ "Sator"
+ ]
+ },
+ "multimediaviewer-repository-local": "Xehetasun gehiago",
+ "multimediaviewer-credit-fallback": "Egileari buruzko informazioa ikusi",
+ "multimediaviewer-multiple-authors": "{{PLURAL:$1|egile bat gehiago|$1 egile gehiago}}",
+ "multimediaviewer-multiple-authors-combine": "$1 eta $2",
+ "multimediaviewer-thumbnail-error-retry": "saiatu berriro",
+ "multimediaviewer-license-cc-pd": "Jabetza Publikoa",
+ "multimediaviewer-license-pd": "Jabetza Publikoa",
+ "multimediaviewer-license-default": "Lizentzia ikusi",
+ "multimediaviewer-permission-title": "Baimenaren xehetasunak",
+ "multimediaviewer-permission-link": "ikusi baldintzak",
+ "multimediaviewer-permission-link-hide": "ezkutatu baldintzak",
+ "multimediaviewer-permission-viewmore": "Ikusi gehiago",
+ "multimediaviewer-help-mmv": "Laguntza",
+ "multimediaviewer-reuse-loading-placeholder": "Kargatzen...",
+ "multimediaviewer-download-preview-link-title": "Nabigatzailean ikusi",
+ "multimediaviewer-html-embed-credit-link-text": "Lotura",
+ "multimediaviewer-view-expanded": "Media Viewerren ireki",
+ "multimediaviewer-download-attribution-cta": "Erakuts iezadazu nola",
+ "multimediaviewer-options-learn-more": "Gehiago ikasi",
+ "multimediaviewer-option-cancel-button": "Utzi"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/fa.json b/www/wiki/extensions/MultimediaViewer/i18n/fa.json
new file mode 100644
index 00000000..d58e1107
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/fa.json
@@ -0,0 +1,126 @@
+{
+ "@metadata": {
+ "authors": [
+ "Armin1392",
+ "Ebraminio",
+ "Mcuteangel",
+ "Omidh",
+ "Reza1615",
+ "Omid.koli",
+ "Alirezaaa",
+ "Macofe",
+ "Ladsgroup"
+ ]
+ },
+ "multimediaviewer-desc": "تصاویر بندانگشتی در اندازهٔ بزرگ‌تر داخل یک رابط کاربری تمام‌صفحه گسترش یابند.",
+ "multimediaviewer-pref": "نمایش‌دهندهٔ رسانه",
+ "multimediaviewer-pref-desc": "تجربهٔ بازدید چندرسانه‌ای شما با این ابزار جدید بهبود می‌یابد و تصاویر را در اندازهٔ بزرگتر در صفحه‌هایی که تصویر بندانگشتی دارند نمایش می‌دهد. تصاویر در پوشش سبک زیباتری نمایش داده می‌شوند و همچنین می‌توانند در اندازهٔ اصلی نمایش داده شوند.",
+ "multimediaviewer-optin-pref": "فعال کردن <span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About نمایشگر رسانه‌ای]</span>",
+ "multimediaviewer-file-page": "رفتن به صفحهٔ مرتبط با پرونده",
+ "multimediaviewer-repository-local": "جزئیات بیشتر",
+ "multimediaviewer-datetime-created": "ایجادشده: $1",
+ "multimediaviewer-datetime-uploaded": "بارگذاری‌شده: $1",
+ "multimediaviewer-credit-fallback": "مشاهدهٔ اطلاعات نویسنده",
+ "multimediaviewer-multiple-authors": "{{PLURAL:$1|یک نویسنده بیشتر|$1 نویسنده بیشتر}}",
+ "multimediaviewer-multiple-authors-combine": "$1 و $2",
+ "multimediaviewer-metadata-error": "نمی‌توان جزئیات تصویر را بارگیری کرد (خطا: $1)",
+ "multimediaviewer-thumbnail-error": "متأسفیم، پرونده نمی‌تواند نمایش یابد",
+ "multimediaviewer-thumbnail-error-description": "به نظر می‌رسد مشکل فنی وجود داشته‌باشد. شما می‌توانید $1 یا $3 اگر همچنان ادامه دارد. خطا: $2",
+ "multimediaviewer-thumbnail-error-retry": "تلاش دوباره",
+ "multimediaviewer-thumbnail-error-report": "مشکل را گزارش دهید",
+ "multimediaviewer-license-cc-by-2.0": "CC BY 2.0",
+ "multimediaviewer-license-cc-by-sa-2.0": "CC BY-SA 2.0",
+ "multimediaviewer-license-cc-by-2.5": "CC BY 2.5",
+ "multimediaviewer-license-cc-by-sa-2.5": "CC BY-SA 2.5",
+ "multimediaviewer-license-cc-by-3.0": "CC BY 3.0",
+ "multimediaviewer-license-cc-by-sa-3.0": "CC BY-SA 3.0",
+ "multimediaviewer-license-cc-pd": "مالکیت عمومی",
+ "multimediaviewer-license-pd": "مالکیت عمومی",
+ "multimediaviewer-license-default": "مشاهده پروانه",
+ "multimediaviewer-permission-title": "جزئیات اجازه‌نامه",
+ "multimediaviewer-permission-link": "مشاهدهٔ شرایط",
+ "multimediaviewer-permission-link-hide": "پنهان‌سازی شرایط",
+ "multimediaviewer-permission-viewmore": "مشاهدهٔ بیشتر",
+ "multimediaviewer-restriction-2257": "این تصویر دارای محتوای جنسی است که ممکن است توسط قوانین حمایت از کودک منع شده‌باشد.",
+ "multimediaviewer-restriction-trademarked": "این تصویر شامل محتوایی است که ممکن است تحت قوانین علامت تجاری باشد.",
+ "multimediaviewer-about-mmv": "درباره",
+ "multimediaviewer-discuss-mmv": "بحث",
+ "multimediaviewer-help-mmv": "کمک",
+ "multimediaviewer-optout-mmv": "غیرفعال‌ساختن نمایش‌دهندهٔ رسانه",
+ "multimediaviewer-optin-mmv": "فعال‌ساختن نمایش‌دهندهٔ رسانه",
+ "multimediaviewer-optout-pending-mmv": "غیرفعال‌ساختن نمایش‌دهندهٔ رسانه",
+ "multimediaviewer-optin-pending-mmv": "فعال‌ساختن نمایش‌دهندهٔ رسانه",
+ "multimediaviewer-optout-help": "نمایش‌دهندهٔ رسانه دیگر برای نمایش تصاویر استفاده نخواهد شد. برای استفادهٔ دوباره از آن، روی دکمهٔ «{{int:multimediaviewer-view-expanded}}» کنار هر تصویر کلیک کنید. سپس روی «{{int:multimediaviewer-optin-mmv}}» کلیک کنید.",
+ "multimediaviewer-optin-help": "نمایشگر رسانه برای نمایش تصاویر استفاده خواهد شد.",
+ "multimediaviewer-geoloc-north": "نو",
+ "multimediaviewer-geoloc-east": "شرق",
+ "multimediaviewer-geoloc-south": "S",
+ "multimediaviewer-geoloc-west": "غرب",
+ "multimediaviewer-geoloc-coords": "$1، $2",
+ "multimediaviewer-geolocation": "مکان: $1",
+ "multimediaviewer-reuse-link": "این پرونده را به اشتراک بگذارید یا جاسازی کنید",
+ "multimediaviewer-reuse-loading-placeholder": "در حال بارگیری...",
+ "multimediaviewer-share-tab": "اشتراک‌گذاری",
+ "multimediaviewer-embed-tab": "جاسازی‌کردن",
+ "multimediaviewer-download-link": "دریافت این پرونده",
+ "multimediaviewer-download-preview-link-title": "مشاهده در مرورگر",
+ "multimediaviewer-download-original-button-name": "دریافت پروندهٔ اصلی",
+ "multimediaviewer-download-small-button-name": "دریافت اندازهٔ کوچک",
+ "multimediaviewer-download-medium-button-name": "دریافت اندازهٔ متوسط",
+ "multimediaviewer-download-large-button-name": "دریافت اندازهٔ بزرگ",
+ "multimediaviewer-link-to-page": "پیوند به صفحهٔ توضیحات پرونده",
+ "multimediaviewer-link-to-file": "پیوند به پروندهٔ اصلی",
+ "multimediaviewer-share-explanation": "نسخه بردارید و به آزادی پیوند را به اشتراک بگذارید",
+ "multimediaviewer-embed-wt": "ویکی‌متن",
+ "multimediaviewer-embed-html": "اچ‌تی‌ام‌ال",
+ "multimediaviewer-embed-explanation": "از این کد برای جاسازی‌کردن پرونده استفاده کنید",
+ "multimediaviewer-text-embed-credit-text-bl": "توسط $1، $2، $3",
+ "multimediaviewer-text-embed-credit-text-b": "توسط $1، $2",
+ "multimediaviewer-text-embed-credit-text-l": "$1، $2",
+ "multimediaviewer-html-embed-credit-text-bl": "توسط $1، $2، $3",
+ "multimediaviewer-html-embed-credit-text-b": "توسط $1، $2",
+ "multimediaviewer-html-embed-credit-text-l": "$1، $2",
+ "multimediaviewer-html-embed-credit-link-text": "پیوند",
+ "multimediaviewer-embed-byline": "توسط $1",
+ "multimediaviewer-embed-license": "تحت پروانهٔ $1.",
+ "multimediaviewer-embed-via": "به وسیلهٔ $1.",
+ "multimediaviewer-default-embed-dimensions": "اندازهٔ تصویر بندانگشتی پیش‌فرض",
+ "multimediaviewer-original-embed-dimensions": "پروندهٔ اصلی $1",
+ "multimediaviewer-large-embed-dimensions": "بزرگ $1",
+ "multimediaviewer-medium-embed-dimensions": "متوسط $1",
+ "multimediaviewer-small-embed-dimensions": "کوچک $1",
+ "multimediaviewer-description-page-button-text": "اطلاعات بیشتر دربارهٔ این پرونده",
+ "multimediaviewer-description-page-popup-text": "اطلاعات بیشتر دربارهٔ این پرونده در $1",
+ "multimediaviewer-commons-subtitle": "مخزن آزاد رسانه",
+ "multimediaviewer-view-expanded": "باز کردن در نمایش‌دهندهٔ رسانه",
+ "multimediaviewer-view-config": "پیکربندی",
+ "multimediaviewer-close-popup-text": "این ابزار را ببند (Ecs)",
+ "multimediaviewer-fullscreen-popup-text": "نمایش در تمام‌صفحه",
+ "multimediaviewer-defullscreen-popup-text": "خروج از تمام‌صفحه",
+ "multimediaviewer-title-popup-text": "توضیحات",
+ "multimediaviewer-credit-popup-text": "اطلاعات منبع و نویسنده",
+ "multimediaviewer-title-popup-text-more": "مشاهدهٔ کامل توضیحات",
+ "multimediaviewer-credit-popup-text-more": "مشاهدهٔ کامل منبع و نویسنده",
+ "multimediaviewer-download-attribution-cta-header": "شما باید به نویسنده نسبت دهید",
+ "multimediaviewer-download-optional-attribution-cta-header": "شما می‌توانید به نویسنده نسبت دهید",
+ "multimediaviewer-download-attribution-cta": "به‌ من نشان بده چگونه",
+ "multimediaviewer-attr-plain": "ساده",
+ "multimediaviewer-attr-html": "HTML",
+ "multimediaviewer-options-tooltip": "فعال یا غیرفعال‌ساختن نمایش‌دهندهٔ رسانه",
+ "multimediaviewer-options-dialog-header": "نمایش‌دهندهٔ رسانه را غیرفعال می‌کنید؟",
+ "multimediaviewer-options-text-header": "این قابلیت مشاهدهٔ همهٔ پرونده‌ها را رد کنید.",
+ "multimediaviewer-options-text-body": "شما می‌توانید آن را بعداً از طریق صفحهٔ جزئیات پرونده فعال کنید.",
+ "multimediaviewer-options-learn-more": "بیشتر بدانید",
+ "multimediaviewer-option-submit-button": "غیرفعال‌ساختن نمایش‌دهندهٔ رسانه",
+ "multimediaviewer-option-cancel-button": "لغو",
+ "multimediaviewer-disable-confirmation-header": "شما نمایش‌دهندهٔ رسانه را غیرفعال کردید",
+ "multimediaviewer-disable-confirmation-text": "دفعهٔ بعد که روی تصویر بندانگشتی در $1 کلیک می‌کنید، به طور مستقیم جزئیات همهٔ پرونده‌ها را می‌بینید.",
+ "multimediaviewer-enable-dialog-header": "نمایش‌دهندهٔ رسانه را فعال می‌کنید؟",
+ "multimediaviewer-enable-text-header": "این قابلیت مشاهدهٔ رسانه را برای همهٔ پرونده‌ها به صورت پیش‌فرض فعال کنید.",
+ "multimediaviewer-enable-submit-button": "فعال‌ساختن نمایش‌دهندهٔ رسانه",
+ "multimediaviewer-enable-confirmation-header": "شما نمایش‌دهندهٔ رسانه را برای همهٔ پرونده‌ها فعال کردید",
+ "multimediaviewer-enable-confirmation-text": "دفعهٔ بعد که روی تصویر بندانگشتی در $1 کلیک می‌کنید، نمایش‌دهندهٔ رسانه استفاده خواهد شد.",
+ "multimediaviewer-enable-alert": "نمایش‌دهندهٔ رسانه اکنون غیرفعال است",
+ "multimediaviewer-disable-info-title": "شما نمایش‌دهندهٔ رسانه را غیرفعال کردید",
+ "multimediaviewer-disable-info": "شما هنوز می‌توانید پرونده‌های منحصربه‌فرد را با نمایش‌دهندهٔ رسانه مشاهده کنید."
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/fi.json b/www/wiki/extensions/MultimediaViewer/i18n/fi.json
new file mode 100644
index 00000000..2585a8aa
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/fi.json
@@ -0,0 +1,87 @@
+{
+ "@metadata": {
+ "authors": [
+ "Elseweyr",
+ "Nike",
+ "Stryn",
+ "ElmA",
+ "Pxos",
+ "MrTapsa",
+ "McSalama",
+ "Pyscowicz",
+ "Pahkiqaz"
+ ]
+ },
+ "multimediaviewer-desc": "Näytä pienoiskuvat suuremmassa koossa kokoruututilassa.",
+ "multimediaviewer-pref": "Media Viewer",
+ "multimediaviewer-pref-desc": "Paranna multimedian katselukokemustasi tällä uudella työkalulla. Se näyttää kuvat suuremmassa koossa sivuilla, joissa on kuvakkeita. Kuvat aukeavat suuremmassa koossa kokoruututilassa, ja ne voidaan näyttää myös täysikokoisina.",
+ "multimediaviewer-optin-pref": "Ota käyttöön <span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About Media Viewer]</span>",
+ "multimediaviewer-file-page": "Siirry tiedoston omalle sivulle",
+ "multimediaviewer-repository-local": "Lisätiedot",
+ "multimediaviewer-datetime-created": "Luotu: $1",
+ "multimediaviewer-datetime-uploaded": "Tallennettu: $1",
+ "multimediaviewer-credit-fallback": "Näytä tekijätiedot",
+ "multimediaviewer-multiple-authors-combine": "$1 ja $2",
+ "multimediaviewer-metadata-error": "Kuvan tietoja ei voitu ladata (virhe: $1)",
+ "multimediaviewer-thumbnail-error": "Pahoittelut, tiedostoa ei voi näyttää",
+ "multimediaviewer-license-cc-pd": "Public Domain",
+ "multimediaviewer-license-pd": "Public Domain",
+ "multimediaviewer-license-default": "Näytä lisenssi",
+ "multimediaviewer-permission-title": "Lisenssitiedot",
+ "multimediaviewer-permission-link": "katso käyttöehdot",
+ "multimediaviewer-permission-link-hide": "piilota termit",
+ "multimediaviewer-permission-viewmore": "Näytä lisää",
+ "multimediaviewer-about-mmv": "Tietoa",
+ "multimediaviewer-discuss-mmv": "Keskustelu",
+ "multimediaviewer-help-mmv": "Ohje",
+ "multimediaviewer-optout-mmv": "Ota Media Viewer pois käytöstä",
+ "multimediaviewer-optin-mmv": "Ota Media Viewer käyttöön",
+ "multimediaviewer-optout-pending-mmv": "Otetaan Media Viewer pois käytöstä",
+ "multimediaviewer-optin-pending-mmv": "Otetaan Media Viewer käyttöön",
+ "multimediaviewer-geolocation": "Sijainti: $1",
+ "multimediaviewer-reuse-link": "Jaa tai upota tämä tiedosto",
+ "multimediaviewer-reuse-loading-placeholder": "Ladataan…",
+ "multimediaviewer-share-tab": "Jaa",
+ "multimediaviewer-embed-tab": "Upota",
+ "multimediaviewer-download-link": "Lataa tämä tiedosto",
+ "multimediaviewer-download-preview-link-title": "Näytä selaimessa",
+ "multimediaviewer-download-original-button-name": "Lataa alkuperäinen tiedosto",
+ "multimediaviewer-download-small-button-name": "Lataa pienikokoisena",
+ "multimediaviewer-download-medium-button-name": "Lataa keskikokoisena",
+ "multimediaviewer-download-large-button-name": "Lataa suurikokoisena",
+ "multimediaviewer-link-to-page": "Linkki tiedoston kuvaussivulle",
+ "multimediaviewer-link-to-file": "Linkki alkuperäiseen tiedostoon",
+ "multimediaviewer-share-explanation": "Kopioi linkki ja levitä sitä vapaasti",
+ "multimediaviewer-embed-wt": "Wikiteksti",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-embed-explanation": "Käytä tätä koodia tiedoston sijoittamiseksi kohteeseen",
+ "multimediaviewer-html-embed-credit-link-text": "Linkki",
+ "multimediaviewer-embed-byline": "Tekijä on $1",
+ "multimediaviewer-embed-license": "Lisensoitu lisenssillä $1.",
+ "multimediaviewer-default-embed-dimensions": "Pienoiskuvien oletuskoko",
+ "multimediaviewer-original-embed-dimensions": "Alkuperäinen tiedosto $1",
+ "multimediaviewer-large-embed-dimensions": "Suuri $1",
+ "multimediaviewer-medium-embed-dimensions": "Keskikokoinen $1",
+ "multimediaviewer-small-embed-dimensions": "Pieni $1",
+ "multimediaviewer-description-page-button-text": "Lisätietoa tästä tiedostosta",
+ "multimediaviewer-description-page-popup-text": "Lisätietoa tästä tiedostosta sivulla $1",
+ "multimediaviewer-commons-subtitle": "Vapaan mediasisällön tietokanta",
+ "multimediaviewer-view-expanded": "Avaa Media Viewerissä",
+ "multimediaviewer-view-config": "Asetukset",
+ "multimediaviewer-close-popup-text": "Sulje tämä työkalu (Esc)",
+ "multimediaviewer-fullscreen-popup-text": "Näytä koko näytössä",
+ "multimediaviewer-defullscreen-popup-text": "Poistu koko näytön tilasta",
+ "multimediaviewer-next-image-alt-text": "Näytä seuraava kuva",
+ "multimediaviewer-prev-image-alt-text": "Näytä edellinen kuva",
+ "multimediaviewer-title-popup-text": "Kuvaus",
+ "multimediaviewer-title-popup-text-more": "Näytä koko kuvaus",
+ "multimediaviewer-download-attribution-cta": "Näytä kuinka",
+ "multimediaviewer-reuse-warning-nonfree": "Tiedosto ei ole vapaasti lisensoitu.",
+ "multimediaviewer-options-learn-more": "Lue lisää",
+ "multimediaviewer-option-cancel-button": "Peru",
+ "multimediaviewer-enable-dialog-header": "Ota Media Viewer käyttöön?",
+ "multimediaviewer-enable-submit-button": "Ota Media Viewer käyttöön",
+ "multimediaviewer-enable-alert": "Media Viewer on nyt poistettu käytöstä",
+ "multimediaviewer-disable-info-title": "Olet poistanut Media Viewerin käytöstä",
+ "multimediaviewer-disable-info": "Voit silti tarkastella yksittäisiä tiedostoja Media Viewerillä."
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/fr.json b/www/wiki/extensions/MultimediaViewer/i18n/fr.json
new file mode 100644
index 00000000..d593bed6
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/fr.json
@@ -0,0 +1,142 @@
+{
+ "@metadata": {
+ "authors": [
+ "Gomoko",
+ "Jean-Frédéric",
+ "Ltrlg",
+ "NemesisIII",
+ "Verdy p",
+ "Sherbrooke",
+ "Seb35",
+ "Macofe",
+ "Windes",
+ "0x010C",
+ "Orikrin1998",
+ "Weft",
+ "Wladek92",
+ "Urhixidur"
+ ]
+ },
+ "multimediaviewer-desc": "Agrandit les vignettes dans une interface en plein écran",
+ "multimediaviewer-pref": "Visionneuse de Médias",
+ "multimediaviewer-pref-desc": "Améliorez votre expérience de visualisation multimédia avec ce nouvel outil. Il affiche les images en grande taille sur les pages qui ont des vignettes. Les images sont affichées dans un joli cadre d’interface en plein écran, et peuvent aussi être affichées en taille maximale.",
+ "multimediaviewer-optin-pref": "Activer <span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About la visionneuse de médias]</span>",
+ "multimediaviewer-file-page": "Aller à la page du fichier correspondant",
+ "multimediaviewer-repository-local": "Plus de détails",
+ "multimediaviewer-datetime-created": "Création : $1",
+ "multimediaviewer-datetime-uploaded": "Téléversé : $1",
+ "multimediaviewer-credit": "$1 — $2",
+ "multimediaviewer-credit-fallback": "Voir les informations sur l’auteur",
+ "multimediaviewer-multiple-authors": "{{PLURAL:$1|un auteur supplémentaire|$1 auteurs supplémentaire}}",
+ "multimediaviewer-multiple-authors-combine": "$1 et $2",
+ "multimediaviewer-metadata-error": "Impossible de charger les données de l’image. (erreur : $1)",
+ "multimediaviewer-thumbnail-error": "Désolé, le fichier ne peut être chargé.",
+ "multimediaviewer-thumbnail-error-description": "Il semble y avoir un problème technique. Vous pouvez $1 ou $3 s'il persiste. Erreur : $2",
+ "multimediaviewer-thumbnail-error-retry": "réessayer",
+ "multimediaviewer-thumbnail-error-report": "signaler le problème",
+ "multimediaviewer-license-cc-pd": "Domaine public",
+ "multimediaviewer-license-pd": "Domaine public",
+ "multimediaviewer-license-default": "Afficher la licence",
+ "multimediaviewer-permission-title": "Détails de la licence",
+ "multimediaviewer-permission-link": "afficher les conditions",
+ "multimediaviewer-permission-link-hide": "masquer les conditions",
+ "multimediaviewer-permission-viewmore": "Voir plus",
+ "multimediaviewer-restriction-2257": "Cette image contient du contenu sexuellement explicite, qui peut être assujettie à la loi de Protection de l'Enfant et la lutte contre l'Obscénité aux États-unis.",
+ "multimediaviewer-restriction-aus-reserve": "Cette image a été prise dans une réserve du Commonwealth australien et ne peut pas être utilisée à des fins commerciales sans autorisation.",
+ "multimediaviewer-restriction-communist": "Cette image contient des insignes communistes qui peuvent être interdits dans certains pays.",
+ "multimediaviewer-restriction-costume": "Cette image dépeint des costumes et peut être soumise à des restrictions légales.",
+ "multimediaviewer-restriction-currency": "Cette image représente la description d'une unité de monnaie et peut être soumise à des restrictions légales.",
+ "multimediaviewer-restriction-design": "La conception du sujet de cette image peut être protégée et soumise à des restrictions légales.",
+ "multimediaviewer-restriction-fan-art": "Cette image est un travail de fan art, et sa réutilisation peut être soumise à des restrictions légales.",
+ "multimediaviewer-restriction-ihl": "Cette image contient des symboles restreints par le Droit International Humanitaire.",
+ "multimediaviewer-restriction-insignia": "Cette image contient des insignes officiels qui peuvent être soumis à des restrictions légales.",
+ "multimediaviewer-restriction-ita-mibac": "Cette image reproduit une propriété appartenant au patrimoine culturel italien et est limitée par la loi italienne.",
+ "multimediaviewer-restriction-nazi": "Cette image contient des insignes nazi ou fascistes qui peuvent être interdits dans certains pays.",
+ "multimediaviewer-restriction-personality": "Cette image contient des personnes qui peuvent avoir des droits qui légalement restreignent certaines réutilisations de l'image sans leur consentement.",
+ "multimediaviewer-restriction-trademarked": "Cette image contient du contenu qui peut être soumis au droit des marques.",
+ "multimediaviewer-restriction-default": "Cette image peut être limitée par des dispositions légales extérieures au droit d'auteur. Voir la page de description du fichier pour les détails.",
+ "multimediaviewer-restriction-default-and-others": "Cette image peut être en outre limitée par d'autres dispositions légales en dehors du droit d'auteur. Voir la page de description du fichier pour les détails.",
+ "multimediaviewer-about-mmv": "À propos",
+ "multimediaviewer-discuss-mmv": "Discussion",
+ "multimediaviewer-help-mmv": "Aide",
+ "multimediaviewer-optout-mmv": "Désactiver la visionneuse de médias",
+ "multimediaviewer-optin-mmv": "Activer la visionneuse de médias",
+ "multimediaviewer-optout-pending-mmv": "Désactiver la visionneuse de médias",
+ "multimediaviewer-optin-pending-mmv": "Activation de la visionneuse de médias",
+ "multimediaviewer-optout-help": "La visionneuse de média ne sera plus utilisée pour afficher les images. Pour l’utiliser de nouveau, cliquez sur le bouton « {{int:multimediaviewer-view-expanded}} » près d’une image. Puis cliquez sur « {{int:multimediaviewer-optin-mmv}} ».",
+ "multimediaviewer-optin-help": "La visionneuse de média sera utilisée pour afficher les images.",
+ "multimediaviewer-geolocation": "Emplacement : $1",
+ "multimediaviewer-reuse-link": "Partager ou inclure ce fichier",
+ "multimediaviewer-reuse-loading-placeholder": "Chargement en cours…",
+ "multimediaviewer-reuse-copy-share": "Sélectionner et copier (si c’est pris en charge) le lien pour partager ce fichier",
+ "multimediaviewer-reuse-copy-embed": "Sélectionner et copier (si c’est pris en charge) le code pour inclure ce fichier",
+ "multimediaviewer-share-tab": "Partager",
+ "multimediaviewer-embed-tab": "Intégrer",
+ "multimediaviewer-download-link": "Télécharger ce fichier",
+ "multimediaviewer-download-preview-link-title": "Afficher dans le navigateur",
+ "multimediaviewer-download-original-button-name": "Télécharger le fichier d’origine",
+ "multimediaviewer-download-small-button-name": "Télécharger en petite taille",
+ "multimediaviewer-download-medium-button-name": "Télécharger en taille moyenne",
+ "multimediaviewer-download-large-button-name": "Télécharger en grande taille",
+ "multimediaviewer-link-to-page": "Lien vers la page de description du fichier",
+ "multimediaviewer-link-to-file": "Lien vers le fichier d’origine",
+ "multimediaviewer-share-explanation": "Copier et partager librement le lien",
+ "multimediaviewer-embed-wt": "Wikitexte",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-embed-explanation": "Utiliser ce code pour intégrer le fichier",
+ "multimediaviewer-text-embed-credit-text-bl": "Par $1, $2, $3",
+ "multimediaviewer-text-embed-credit-text-b": "Par $1, $2",
+ "multimediaviewer-html-embed-credit-text-bl": "Par $1, $2, $3",
+ "multimediaviewer-html-embed-credit-text-b": "Par $1, $2",
+ "multimediaviewer-html-embed-credit-link-text": "Lien",
+ "multimediaviewer-embed-byline": "Par $1",
+ "multimediaviewer-embed-license": "Sous licence $1.",
+ "multimediaviewer-embed-via": "Via $1.",
+ "multimediaviewer-default-embed-dimensions": "Taille de vignette par défaut",
+ "multimediaviewer-original-embed-dimensions": "Fichier d’origine $1",
+ "multimediaviewer-large-embed-dimensions": "Grand $1",
+ "multimediaviewer-medium-embed-dimensions": "Moyen $1",
+ "multimediaviewer-small-embed-dimensions": "Petit $1",
+ "multimediaviewer-embed-dimensions": "$1 × $2 px",
+ "multimediaviewer-description-page-button-text": "Plus de détails sur ce fichier",
+ "multimediaviewer-description-page-popup-text": "Plus de détails sur ce fichier en $1",
+ "multimediaviewer-commons-subtitle": "La médiathèque libre",
+ "multimediaviewer-view-expanded": "Ouvrir dans le Visualiseur de médias",
+ "multimediaviewer-view-config": "Configuration",
+ "multimediaviewer-close-popup-text": "Fermer cet outil (Esc)",
+ "multimediaviewer-fullscreen-popup-text": "Afficher en plein écran",
+ "multimediaviewer-defullscreen-popup-text": "Sortir du plein écran",
+ "multimediaviewer-next-image-alt-text": "Afficher la prochaine image",
+ "multimediaviewer-prev-image-alt-text": "Afficher l'image précédente",
+ "multimediaviewer-title-popup-text": "Description",
+ "multimediaviewer-credit-popup-text": "Auteur et source de l’information",
+ "multimediaviewer-title-popup-text-more": "Afficher la description complète",
+ "multimediaviewer-credit-popup-text-more": "Afficher au complet l’auteur et la source",
+ "multimediaviewer-download-attribution-cta-header": "Vous devez attribuer l’auteur",
+ "multimediaviewer-download-optional-attribution-cta-header": "Vous pouvez attribuer l'auteur",
+ "multimediaviewer-download-attribution-cta": "Me montrer comment",
+ "multimediaviewer-download-attribution-copy": "Sélectionner et copier (si pris en charge) le texte d'attribution pour ce fichier",
+ "multimediaviewer-reuse-warning-deletion": "Ce fichier est considéré comme à détruire.",
+ "multimediaviewer-reuse-warning-nonfree": "Ce fichier ne possède pas la licence libre.",
+ "multimediaviewer-reuse-warning-noattribution": "Ce fichier n'a pas d'information d'attribution.",
+ "multimediaviewer-reuse-warning-generic": "Vérifier [$1 ses détails] avant de l'utiliser.",
+ "multimediaviewer-attr-plain": "Simple",
+ "multimediaviewer-options-tooltip": "Activer ou désactiver la visionneuse de médias",
+ "multimediaviewer-options-dialog-header": "Désactiver la visionneuse de médias",
+ "multimediaviewer-options-text-header": "Ignorer cette fonctionnalité de visualisation pour tous les fichiers.",
+ "multimediaviewer-options-text-body": "Vous pourrez l'activer plus tard à partir de la page de détails des fichiers.",
+ "multimediaviewer-options-learn-more": "En savoir plus",
+ "multimediaviewer-option-submit-button": "Désactiver la visionneuse de médias",
+ "multimediaviewer-option-cancel-button": "Annuler",
+ "multimediaviewer-disable-confirmation-header": "Vous avez désactivé la visionneuse de médias",
+ "multimediaviewer-disable-confirmation-text": "La prochaine fois que vous cliquerez sur une vignette sur $1, vous verrez directement la page complète d'information sur le fichier.",
+ "multimediaviewer-enable-dialog-header": "Activer la visionneuse de médias ?",
+ "multimediaviewer-enable-text-header": "Activer cette fonctionnalité de visualisation pour l'ensemble des fichiers par défaut.",
+ "multimediaviewer-enable-submit-button": "Activer la visionneuse de médias",
+ "multimediaviewer-enable-confirmation-header": "Vous avez activé la visionneuse de médias pour tous les fichiers",
+ "multimediaviewer-enable-confirmation-text": "La prochaine fois que vous cliquerez sur une vignette sur $1, la visionneuse de médias sera utilisée.",
+ "multimediaviewer-enable-alert": "La visionneuse de médias est maintenant désactivée",
+ "multimediaviewer-disable-info-title": "Vous avez désactivé la visionneuse de médias",
+ "multimediaviewer-disable-info": "Vous pouvez toujours consulter des fichiers individuels avec la visionneuse de médias.",
+ "multimediaviewer-errorreport-privacywarning": "Les détails sur l’erreur sont attachés au rapport, qui sera visible publiquement. Si cela ne vous convient pas, vous pouvez modifier le rapport ci-dessous et supprimer toutes les données que vous ne voulez pas partager."
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/frr.json b/www/wiki/extensions/MultimediaViewer/i18n/frr.json
new file mode 100644
index 00000000..0446b8c2
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/frr.json
@@ -0,0 +1,9 @@
+{
+ "@metadata": {
+ "authors": [
+ "Murma174"
+ ]
+ },
+ "multimediaviewer-pref": "Media Viewer",
+ "multimediaviewer-optin-pref": "Di <span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About MediaViewer]</span> aktiwiare"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/fy.json b/www/wiki/extensions/MultimediaViewer/i18n/fy.json
new file mode 100644
index 00000000..3a6914d6
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/fy.json
@@ -0,0 +1,15 @@
+{
+ "@metadata": {
+ "authors": [
+ "Robin0van0der0vliet",
+ "Robin van der Vliet"
+ ]
+ },
+ "multimediaviewer-multiple-authors-combine": "$1 en $2",
+ "multimediaviewer-help-mmv": "Help",
+ "multimediaviewer-geolocation": "Lokaasje: $1",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-large-embed-dimensions": "Grut $1",
+ "multimediaviewer-small-embed-dimensions": "Lyts $1",
+ "multimediaviewer-title-popup-text": "Beskriuwing"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/gd.json b/www/wiki/extensions/MultimediaViewer/i18n/gd.json
new file mode 100644
index 00000000..825a4a1a
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/gd.json
@@ -0,0 +1,123 @@
+{
+ "@metadata": {
+ "authors": [
+ "GunChleoc",
+ "Akerbeltz"
+ ]
+ },
+ "multimediaviewer-desc": "Thoir meud nas motha air dealbhagan air eadar-aghaidh làn-sgrìn.",
+ "multimediaviewer-pref": "Sealladair nam meadhanan",
+ "multimediaviewer-pref-desc": "Thoir piseach air sealladh air meadhanan leis an inneal ùr seo. Seallaidh e dealbhan as motha air duilleagan aig a bheil dealbhagan. Thèid dealbhan a shealltainn air tar-chòmhdachadh làn-sgrìn agus gabhaidh an sealltainn le làn-leud cuideachd.",
+ "multimediaviewer-optin-pref": "Cuir <span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About sealladair nam headhanan]</span> an comas",
+ "multimediaviewer-file-page": "Rach gu dhuilleag an fhaidhle seo",
+ "multimediaviewer-repository-local": "Barrachd fiosrachaidh",
+ "multimediaviewer-datetime-created": "Air a chruthachadh: $1",
+ "multimediaviewer-datetime-uploaded": "Air a luchdadh suas: $1",
+ "multimediaviewer-credit": "$1 - $2",
+ "multimediaviewer-credit-fallback": "Seall fiosrachadh an ùghdair",
+ "multimediaviewer-multiple-authors": "{{PLURAL:$1|$1 ùghdar eile|$1 ùghdar eile|$1 ùghdar eilean|$1 ùghdar eile}}",
+ "multimediaviewer-multiple-authors-combine": "$1 agus $2",
+ "multimediaviewer-metadata-error": "Cha deach leinn fiosrachadh an deilbh a luchdadh (mearachd: $1)",
+ "multimediaviewer-thumbnail-error": "Tha sinn duilich ach cha chan urrainn dhuinn am faidhle a shealltainn",
+ "multimediaviewer-thumbnail-error-description": "Tha coltas gu bheil duilgheadas teicnigeach ann. 'S urrainn dhut $1 no $3 ma mhaireas e. Mearachd: $2",
+ "multimediaviewer-thumbnail-error-retry": "feuchainn ris a-rithist",
+ "multimediaviewer-thumbnail-error-report": "aithris a dhèanamh air an duilgheadas",
+ "multimediaviewer-license-cc-by-1.0": "CC BY 1.0",
+ "multimediaviewer-license-cc-sa-1.0": "CC SA 1.0",
+ "multimediaviewer-license-cc-by-sa-1.0": "CC BY-SA 1.0",
+ "multimediaviewer-license-cc-by-2.0": "CC BY 2.0",
+ "multimediaviewer-license-cc-by-sa-2.0": "CC BY-SA 2.0",
+ "multimediaviewer-license-cc-by-2.1": "CC BY 2.1",
+ "multimediaviewer-license-cc-by-sa-2.1": "CC BY-SA 2.1",
+ "multimediaviewer-license-cc-by-2.5": "CC BY 2.5",
+ "multimediaviewer-license-cc-by-sa-2.5": "CC BY-SA 2.5",
+ "multimediaviewer-license-cc-by-3.0": "CC BY 3.0",
+ "multimediaviewer-license-cc-by-sa-3.0": "CC BY-SA 3.0",
+ "multimediaviewer-license-cc-by-4.0": "CC BY 4.0",
+ "multimediaviewer-license-cc-by-sa-4.0": "CC BY-SA 4.0",
+ "multimediaviewer-license-cc-pd": "Public Domain",
+ "multimediaviewer-license-cc-zero": "CC 0",
+ "multimediaviewer-license-pd": "Public Domain",
+ "multimediaviewer-license-default": "Seall an ceadachas",
+ "multimediaviewer-permission-title": "Fiosrachadh a' chead",
+ "multimediaviewer-permission-link": "seall na teirmichean",
+ "multimediaviewer-permission-link-hide": "falaich na teirmichean",
+ "multimediaviewer-permission-viewmore": "Seall barrachd",
+ "multimediaviewer-about-mmv": "Mu dhèidhinn",
+ "multimediaviewer-discuss-mmv": "Deasbaireachd",
+ "multimediaviewer-help-mmv": "Cobhair",
+ "multimediaviewer-optout-mmv": "Cuir sealladair nam meadhanan à comas",
+ "multimediaviewer-optin-mmv": "Cuir an Sealladair mheadhanan an comas",
+ "multimediaviewer-optout-pending-mmv": "A' cur sealladair nam meadhanan à comas",
+ "multimediaviewer-optin-pending-mmv": "A' cur an t-Sealladair mheadhanan an comas",
+ "multimediaviewer-optout-help": "Cha dèid sealladair nam meadhannan a chleachdadh tuilleadh gus dealbhan a shealltainn. Gus a chleachdadh a-rithist, briog air a' phutan \"{{int:multimediaviewer-view-expanded}}\" ri taobh dealbh sam bith. Briog air \"{{int:multimediaviewer-optin-mmv}}\" an uairsin.",
+ "multimediaviewer-optin-help": "Thèid an sealladair mheadhanan a chleachdadh gus dealbhan a shealltainn.",
+ "multimediaviewer-geoloc-north": "T",
+ "multimediaviewer-geoloc-east": "E",
+ "multimediaviewer-geoloc-south": "D",
+ "multimediaviewer-geoloc-west": "I",
+ "multimediaviewer-geoloc-coord": "$1° $2′ $3″ $4",
+ "multimediaviewer-geoloc-coords": "$1, $2",
+ "multimediaviewer-geolocation": "Ionad: $1",
+ "multimediaviewer-reuse-link": "Co-roinn no leabaich am faidhle seo",
+ "multimediaviewer-reuse-loading-placeholder": "'Ga luchdadh…",
+ "multimediaviewer-share-tab": "Co-roinn",
+ "multimediaviewer-embed-tab": "Leabaich",
+ "multimediaviewer-download-link": "Luchdaich a-nuas am faidhle seo",
+ "multimediaviewer-download-preview-link-title": "Seall sa bhrabhsair",
+ "multimediaviewer-download-original-button-name": "Luchdaich a-nuas am faidhle tùsail",
+ "multimediaviewer-download-small-button-name": "Luchdaich a-nuas le meud beag",
+ "multimediaviewer-download-medium-button-name": "Luchdaich a-nuas le meud meadhanach",
+ "multimediaviewer-download-large-button-name": "Luchdaich a-nuas le meud mòr",
+ "multimediaviewer-link-to-page": "Dèan ceangal ri duilleag mìneachadh an fhaidhle",
+ "multimediaviewer-link-to-file": "Dèan ceangal ris an fhaidhle tùsail",
+ "multimediaviewer-share-explanation": "Dèan lethbhreac 's co-roinn an ceangal gu saor",
+ "multimediaviewer-embed-wt": "Wikitext",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-embed-explanation": "Cleachd an còd seo gus am faidhle a leabachadh",
+ "multimediaviewer-text-embed-credit-text-bl": "Le $1, $2, $3",
+ "multimediaviewer-text-embed-credit-text-b": "le $1, $2",
+ "multimediaviewer-html-embed-credit-text-bl": "Le $1, $2, $3",
+ "multimediaviewer-html-embed-credit-text-b": "Le $1, $2",
+ "multimediaviewer-embed-byline": "Le $1",
+ "multimediaviewer-embed-license": "Fo cheadachas $1.",
+ "multimediaviewer-embed-via": "Slighe $1.",
+ "multimediaviewer-default-embed-dimensions": "Meud bunaiteach na dealbhaige",
+ "multimediaviewer-original-embed-dimensions": "Am faidhle tùsail $1",
+ "multimediaviewer-large-embed-dimensions": "Mòr $1",
+ "multimediaviewer-medium-embed-dimensions": "Meadhanach $1",
+ "multimediaviewer-small-embed-dimensions": "Beag $1",
+ "multimediaviewer-embed-dimensions": "$1 × $2 px",
+ "multimediaviewer-embed-dimensions-separated": "- $1",
+ "multimediaviewer-description-page-button-text": "Barrachd fiosrachaidh air an fhaidhle seo",
+ "multimediaviewer-description-page-popup-text": "Barrachd fiosrachaidh air an fhaidhle seo air $1",
+ "multimediaviewer-commons-subtitle": "Ionad-tasgaidh mheadhanan saora",
+ "multimediaviewer-view-expanded": "Fosgail ann an sealladair nam meadhanan",
+ "multimediaviewer-view-config": "Rèiteachadh",
+ "multimediaviewer-close-popup-text": "Dùin an t-inneal seo (Esc)",
+ "multimediaviewer-fullscreen-popup-text": "Seall air làn-sgrìn",
+ "multimediaviewer-defullscreen-popup-text": "Fàg an làn-sgrìn",
+ "multimediaviewer-title-popup-text": "Tuairisgeul",
+ "multimediaviewer-credit-popup-text": "Fiosrachadh mun ùghdar 's tùs",
+ "multimediaviewer-title-popup-text-more": "Seall an tuairisgeul slàn",
+ "multimediaviewer-credit-popup-text-more": "Seall ùghdar is tùs slàn",
+ "multimediaviewer-download-attribution-cta-header": "Feumaidh tu urram a thoirt air an ùghdar",
+ "multimediaviewer-download-attribution-cta": "Seall dhomh ciamar",
+ "multimediaviewer-attr-plain": "Lom",
+ "multimediaviewer-attr-html": "HTML",
+ "multimediaviewer-options-tooltip": "Cuir sealladair nam meadhanan an comas no à comas",
+ "multimediaviewer-options-dialog-header": "A bheil thu airson an sealladair mheadhanan a chur à comas?",
+ "multimediaviewer-options-text-header": "Leig seachad am feart seallaidh seo airson a h-uile faidhle.",
+ "multimediaviewer-options-text-body": "'S urrainn dhut a chur an comas uair sam bith air duilleag fiosrachaidh an deilbh.",
+ "multimediaviewer-options-learn-more": "Barrachd fiosrachaidh",
+ "multimediaviewer-option-submit-button": "Cuir sealladair nam meadhanan à comas",
+ "multimediaviewer-option-cancel-button": "Sguir dheth",
+ "multimediaviewer-disable-confirmation-header": "Tha thu air sealladair nam meadhanan a chur à comas",
+ "multimediaviewer-disable-confirmation-text": "An ath-thuras a bhriogas tu air dealbhag air $1, chì thu fiosrachadh slàn air an fhaidhle sa bhad.",
+ "multimediaviewer-enable-dialog-header": "A bheil thu airson sealladair nam meadhanan a chur an comas?",
+ "multimediaviewer-enable-text-header": "Cuir am feart seo airson meadhanan a shealltainn an comas airson a h-uile faidhle a ghnàth.",
+ "multimediaviewer-enable-submit-button": "Cuir sealladair nam meadhanan an comas",
+ "multimediaviewer-enable-confirmation-text": "An ath thuras a bhriogas tu air dealbhag air $1, thèid sealladair nam meadhanan a chleachdadh.",
+ "multimediaviewer-disable-info-title": "Tha thu air sealladair nam meadhanan a chur à comas",
+ "multimediaviewer-disable-info": "'S urrainn dhut faidhlichean fa leth a shealltainn ann an sealladair nam meadhanan fhathast."
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/gl.json b/www/wiki/extensions/MultimediaViewer/i18n/gl.json
new file mode 100644
index 00000000..2f339e33
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/gl.json
@@ -0,0 +1,131 @@
+{
+ "@metadata": {
+ "authors": [
+ "Toliño",
+ "Vivaelcelta",
+ "Elisardojm",
+ "Macofe",
+ "Banjo"
+ ]
+ },
+ "multimediaviewer-desc": "Expande as miniaturas ata un tamaño maior dentro dunha interface a pantalla completa.",
+ "multimediaviewer-pref": "Visor de ficheiros multimedia",
+ "multimediaviewer-pref-desc": "Mellore a súa experiencia de visualización de ficheiros multimedia con esta nova ferramenta. Mostra as imaxes nun tamaño maior nas páxinas que teñen miniaturas. As imaxes móstranse nun visor a pantalla completa agradable e as imaxes tamén se poden ver a tamaño completo.",
+ "multimediaviewer-optin-pref": "Activar o <span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About visor de ficheiros multimedia]</span>",
+ "multimediaviewer-file-page": "Ir á páxina de ficheiro correspondente",
+ "multimediaviewer-repository-local": "Máis detalles",
+ "multimediaviewer-datetime-created": "Creación: $1",
+ "multimediaviewer-datetime-uploaded": "Subido o $1",
+ "multimediaviewer-credit-fallback": "Ver a información do autor",
+ "multimediaviewer-multiple-authors": "{{PLURAL:$1|un autor máis|$1 autores máis}}",
+ "multimediaviewer-multiple-authors-combine": "$1 e $2",
+ "multimediaviewer-metadata-error": "Non se puideron cargar os detalles da imaxe (erro: $1)",
+ "multimediaviewer-thumbnail-error": "Sentímolo, non se pode mostrar o ficheiro",
+ "multimediaviewer-thumbnail-error-description": "Semella haber un problema técnico. Pode $1 ou $3 se persiste. Erro: $2",
+ "multimediaviewer-thumbnail-error-retry": "reintentar",
+ "multimediaviewer-thumbnail-error-report": "informar sobre o problema",
+ "multimediaviewer-license-cc-pd": "Dominio público",
+ "multimediaviewer-license-pd": "Dominio público",
+ "multimediaviewer-license-default": "Ver a licenza",
+ "multimediaviewer-permission-title": "Detalles dos permisos",
+ "multimediaviewer-permission-link": "ver os termos",
+ "multimediaviewer-permission-link-hide": "agochar os termos",
+ "multimediaviewer-permission-viewmore": "Ollar máis",
+ "multimediaviewer-restriction-2257": "Esta imaxe contén contido sexual explícito que pode ser obxecto da ''Child Protection and Obscenity Enforcement Act'' nos Estados Unidos de América.",
+ "multimediaviewer-restriction-aus-reserve": "Esta imaxe foi realizada nunha reserva da Commonwealth Australiana e non pode ser usada con fins comerciais sen autorización previa.",
+ "multimediaviewer-restriction-communist": "Esta imaxe contén a insignia Comunista que pode estar prohibida nalgúns países.",
+ "multimediaviewer-restriction-costume": "Esta imaxe mostra disfraces, uniformes ou traxes e pode ser obxecto de restricións legais.",
+ "multimediaviewer-restriction-currency": "Esta imaxe mostra unha unidade de moeda e pode ser obxecto de restricións legais.",
+ "multimediaviewer-restriction-design": "O deseño do obxecto desta imaxe pode ter copyright e estar suxeito a restricións legais.",
+ "multimediaviewer-restriction-fan-art": "Esta imaxe é un traballo artístico dun fan, e o reuso pode estar suxeito a restricións legais.",
+ "multimediaviewer-restriction-ihl": "Esta imaxe contén símbolos restrinxidos pola Lei Internacional Humanitaria.",
+ "multimediaviewer-restriction-insignia": "Esta imaxe contén unha insignia oficial que pode estar suxeita a restricións legais.",
+ "multimediaviewer-restriction-ita-mibac": "Esta imaxe reproduce unha propiedade pertencente ó patrimonio cultural Italiano e está restrinxida pola lei Italiana.",
+ "multimediaviewer-restriction-nazi": "Esta imaxe contén a insignia Nazi ou doutras asociacións fascistas, que poden estar prohibidas nalgúns países.",
+ "multimediaviewer-restriction-personality": "Esta imaxe contén persoas que poden ter dereitos que legalmente restrinxen certas reutilizacións da imaxe sen o seu consentimento.",
+ "multimediaviewer-restriction-trademarked": "Esta imaxe inclúe contido que pode estar suxeito ás leis de marcas.",
+ "multimediaviewer-restriction-default": "Esta imaxe pode estar restrinxida por disposicións legais externas ós dereitos de autor. Consulte a páxina de descrición do ficheiro para obter máis detalles.",
+ "multimediaviewer-restriction-default-and-others": "Esta imaxe pode ter máis restricións por outras disposicións legais externas ós dereitos de autor. Consulte a páxina de descrición do ficheiro para obter máis detalles.",
+ "multimediaviewer-about-mmv": "Acerca de",
+ "multimediaviewer-discuss-mmv": "Conversa",
+ "multimediaviewer-help-mmv": "Axuda",
+ "multimediaviewer-optout-mmv": "Desactivar o visor de ficheiros multimedia",
+ "multimediaviewer-optin-mmv": "Activar o visor de ficheiros multimedia",
+ "multimediaviewer-optout-pending-mmv": "Desactivando o visor de ficheiros multimedia",
+ "multimediaviewer-optin-pending-mmv": "Activando o visor de ficheiros multimedia",
+ "multimediaviewer-optout-help": "O visor de ficheiros multimedia xa non se usará para mostrar as imaxes. Para utilizalo de novo, prema no botón \"{{int:multimediaviewer-view-expanded}}\" que hai en cada imaxe. Logo, prema en \"{{int:multimediaviewer-optin-mmv}}\".",
+ "multimediaviewer-optin-help": "O visor de ficheiros multimedia mostrará as imaxes.",
+ "multimediaviewer-geolocation": "Localización: $1",
+ "multimediaviewer-reuse-link": "Compartir ou incluír este ficheiro",
+ "multimediaviewer-reuse-loading-placeholder": "Cargando…",
+ "multimediaviewer-reuse-copy-share": "Seleccionar e copiar (se está soportado) a ligazón para compartir este ficheiro",
+ "multimediaviewer-reuse-copy-embed": "Seleccionar e copiar (se está soportado) o código para incluír este ficheiro",
+ "multimediaviewer-share-tab": "Compartir",
+ "multimediaviewer-embed-tab": "Incorporar",
+ "multimediaviewer-download-link": "Descargar este ficheiro",
+ "multimediaviewer-download-preview-link-title": "Mostrar no navegador",
+ "multimediaviewer-download-original-button-name": "Descargar o ficheiro orixinal",
+ "multimediaviewer-download-small-button-name": "Descargar en tamaño pequeno",
+ "multimediaviewer-download-medium-button-name": "Descargar en tamaño mediano",
+ "multimediaviewer-download-large-button-name": "Descargar en tamaño grande",
+ "multimediaviewer-link-to-page": "Ligazón cara a páxina de descrición do ficheiro",
+ "multimediaviewer-link-to-file": "Ligazón cara ao ficheiro orixinal",
+ "multimediaviewer-share-explanation": "Copie e comparta libremente a ligazón",
+ "multimediaviewer-embed-wt": "Texto wiki",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-embed-explanation": "Use este código para incorporar o ficheiro",
+ "multimediaviewer-text-embed-credit-text-bl": "De $1, $2, $3",
+ "multimediaviewer-text-embed-credit-text-b": "De $1, $2",
+ "multimediaviewer-html-embed-credit-text-bl": "De $1, $2, $3",
+ "multimediaviewer-html-embed-credit-text-b": "De $1, $2",
+ "multimediaviewer-html-embed-credit-link-text": "Ligazón",
+ "multimediaviewer-embed-byline": "Por $1",
+ "multimediaviewer-embed-license": "Baixo a licenza $1.",
+ "multimediaviewer-embed-via": "A través de $1.",
+ "multimediaviewer-default-embed-dimensions": "Tamaño de miniatura predeterminado",
+ "multimediaviewer-original-embed-dimensions": "Ficheiro orixinal $1",
+ "multimediaviewer-large-embed-dimensions": "Grande $1",
+ "multimediaviewer-medium-embed-dimensions": "Mediano $1",
+ "multimediaviewer-small-embed-dimensions": "Pequeno $1",
+ "multimediaviewer-description-page-button-text": "Máis detalles sobre o ficheiro",
+ "multimediaviewer-description-page-popup-text": "Máis detalles sobre o ficheiro en $1",
+ "multimediaviewer-commons-subtitle": "O repositorio multimedia libre",
+ "multimediaviewer-view-expanded": "Abrir no visor de ficheiros multimedia",
+ "multimediaviewer-view-config": "Configuración",
+ "multimediaviewer-close-popup-text": "Pechar a ferramenta (Esc)",
+ "multimediaviewer-fullscreen-popup-text": "Mostrar a pantalla completa",
+ "multimediaviewer-defullscreen-popup-text": "Saír da pantalla completa",
+ "multimediaviewer-next-image-alt-text": "Amosar a imaxe seguinte",
+ "multimediaviewer-prev-image-alt-text": "Amosar a imaxe previa",
+ "multimediaviewer-title-popup-text": "Descrición",
+ "multimediaviewer-credit-popup-text": "Autor e información da orixe",
+ "multimediaviewer-title-popup-text-more": "Ver a descrición completa",
+ "multimediaviewer-credit-popup-text-more": "Ver o autor e a orixe completos",
+ "multimediaviewer-download-attribution-cta-header": "Cómpre darlle recoñecemento ao autor",
+ "multimediaviewer-download-optional-attribution-cta-header": "Pode darlle recoñecemento ao autor",
+ "multimediaviewer-download-attribution-cta": "Mostrádeme como",
+ "multimediaviewer-download-attribution-copy": "Seleccionar e copiar (se está soportado) o texto de atribución deste ficheiro",
+ "multimediaviewer-reuse-warning-deletion": "Este ficheiro está a ser considerado para borrado.",
+ "multimediaviewer-reuse-warning-nonfree": "Este ficheiro non ten unha licenza libre.",
+ "multimediaviewer-reuse-warning-noattribution": "Este ficheiro non ten información de atribución.",
+ "multimediaviewer-reuse-warning-generic": "Compruebe [$1 os seus detalles] antes de utilizalo.",
+ "multimediaviewer-attr-plain": "Simple",
+ "multimediaviewer-options-tooltip": "Activar ou desactivar o visor de ficheiros multimedia",
+ "multimediaviewer-options-dialog-header": "Quere desactivar o visor de ficheiros multimedia?",
+ "multimediaviewer-options-text-header": "Omitir esta función de visualización para todos os ficheiros.",
+ "multimediaviewer-options-text-body": "Pode activalo máis adiante a través da páxina de detalles do ficheiro.",
+ "multimediaviewer-options-learn-more": "Máis información",
+ "multimediaviewer-option-submit-button": "Desactivar o visor de ficheiros multimedia",
+ "multimediaviewer-option-cancel-button": "Cancelar",
+ "multimediaviewer-disable-confirmation-header": "Desactivou o visor de ficheiros multimedia",
+ "multimediaviewer-disable-confirmation-text": "A vindeira vez que prema nunha miniatura en $1 verá directamente todos os detalles do ficheiro.",
+ "multimediaviewer-enable-dialog-header": "Quere activar o visor de ficheiros multimedia?",
+ "multimediaviewer-enable-text-header": "Activar esta función de visualización para todos os ficheiros por defecto.",
+ "multimediaviewer-enable-submit-button": "Activar o visor de ficheiros multimedia",
+ "multimediaviewer-enable-confirmation-header": "Activou o visor de ficheiros multimedia para todos os ficheiros",
+ "multimediaviewer-enable-confirmation-text": "A vindeira vez que prema nunha miniatura en $1 usarase o visor de ficheiros multimedia.",
+ "multimediaviewer-enable-alert": "O visor de ficheiros multimedia está agora desactivado",
+ "multimediaviewer-disable-info-title": "Desactivou o visor de ficheiros multimedia",
+ "multimediaviewer-disable-info": "Pode seguir vendo algúns ficheiros co visor de ficheiros multimedia.",
+ "multimediaviewer-errorreport-privacywarning": "Os detalles do erro achegáronse no informe, que será visible publicamente. Se non concorda con isto, pode editar o informe de abaixo e eliminar todos os datos que non queira compartir."
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/gu.json b/www/wiki/extensions/MultimediaViewer/i18n/gu.json
new file mode 100644
index 00000000..69e7cef2
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/gu.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "KartikMistry"
+ ]
+ },
+ "multimediaviewer-permission-viewmore": "વધુ જુઓ"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/he.json b/www/wiki/extensions/MultimediaViewer/i18n/he.json
new file mode 100644
index 00000000..b10e4951
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/he.json
@@ -0,0 +1,138 @@
+{
+ "@metadata": {
+ "authors": [
+ "Amire80",
+ "Neukoln",
+ "בנימין",
+ "Yona b",
+ "Eladk",
+ "ערן",
+ "Guycn2"
+ ]
+ },
+ "multimediaviewer-desc": "הגדלת תמונות ממוזערות למסך מלא.",
+ "multimediaviewer-pref": "מציג מדיה",
+ "multimediaviewer-pref-desc": "הכלי החדש הזה משפר את חוויית המולטימדיה שלך. הוא מציג תמונות מוגדלות בדפים עם תמונות ממוזערות. התמונות מוצגות בשכבה במסך מלא וניתן להציג אותן גם בגודל מלא.",
+ "multimediaviewer-optin-pref": "הפעלת <span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About מציג המדיה]</span>",
+ "multimediaviewer-file-page": "מעבר אל דף הקובץ המתאים",
+ "multimediaviewer-repository-local": "פרטים נוספים",
+ "multimediaviewer-datetime-created": "נוצר: $1",
+ "multimediaviewer-datetime-uploaded": "הועלה: $1",
+ "multimediaviewer-credit-fallback": "הצגת מידע על היוצר",
+ "multimediaviewer-multiple-authors": "{{PLURAL:$1|ועוד יוצר אחד|ועוד $1 יוצרים}}",
+ "multimediaviewer-multiple-authors-combine": "$1 $2",
+ "multimediaviewer-metadata-error": "לא היה ניתן לטעון את פרטי התמונה. (שגיאה: $1)",
+ "multimediaviewer-thumbnail-error": "סליחה, לא ניתן להציג את הקובץ",
+ "multimediaviewer-thumbnail-error-description": "נראה שיש בעיה טכנית. באפשרותך $1 או $3 אם היא ממשיכה. שגיאה: $2",
+ "multimediaviewer-thumbnail-error-retry": "לנסות שוב",
+ "multimediaviewer-thumbnail-error-report": "לדווח את הבעיה",
+ "multimediaviewer-license-cc-pd": "נחלת הכלל",
+ "multimediaviewer-license-pd": "נחלת הכלל",
+ "multimediaviewer-license-default": "הצגת הרישיון",
+ "multimediaviewer-permission-title": "פרטי ההרשאה",
+ "multimediaviewer-permission-link": "הצגת התנאים",
+ "multimediaviewer-permission-link-hide": "הסתרת התנאים",
+ "multimediaviewer-permission-viewmore": "להראות עוד",
+ "multimediaviewer-restriction-2257": "התמונה הזאת מכילה תוכן מיני שעשוי להיות מושא לחוק להגנת הילד ואכיפת המוסר של ארצות הברית.",
+ "multimediaviewer-restriction-aus-reserve": "התמונה הזאת צולמה בשמורה של חבר העמים האוסטרלי ואין יכולה לשמש לרווח מסחרי ללא רשות.",
+ "multimediaviewer-restriction-communist": "התמונה הזאת מכילה סמלים קומוניסטיים שעשויים להיות אסורים במספר מדינות.",
+ "multimediaviewer-restriction-costume": "התמונה הזאת מציגה תחפושות ועשויה להיות מושא למגבלות משפטיות.",
+ "multimediaviewer-restriction-currency": "התמונה הזאת מציגה יחידת מטבע ועשויה להיות מושא למגבלות משפטיות.",
+ "multimediaviewer-restriction-design": "העיצוב של נושא התמונה הזאת יכול להיות מוגבל בזכויות יוצרים ולהיות מושא למגבלות משפטיות.",
+ "multimediaviewer-restriction-fan-art": "התמונה הזאת היא יצירת אמנות מעריצים, ושימוש חוזר בה עשוי להיות מושא למגבלות משפטיות.",
+ "multimediaviewer-restriction-ihl": "התמונה הזאת מכילה סמלים המוגבלים בחוק ההומניטרי הבין־לאומי.",
+ "multimediaviewer-restriction-insignia": "התמונה הזאת מכילה סמלים רשמיים שעשויים להיות מושא למגבלות משפטיות.",
+ "multimediaviewer-restriction-ita-mibac": "התמונה הזאת מציגה עותק של רכוש ששייך למורשת תרבותית איטלקית ומוגבל בחוק האיטלקי.",
+ "multimediaviewer-restriction-nazi": "המונה הזאת מכילה סמלים נאציים או פשיסטיים אחרים, שעשויים להיות אסורים במדינות מסוימות.",
+ "multimediaviewer-restriction-personality": "התמונה הזאת מכילה אנשים שיכולות להיות להם זכויות שמגבילים באופן משפטי שימושים חוזרים מסוימים של התמונה ללא הסכמה.",
+ "multimediaviewer-restriction-trademarked": "התמונה הזאת מכילה תוכן שאולי נתון לחוקי זכויות יוצרים.",
+ "multimediaviewer-restriction-default": "התמונה הזאת יכולה להיות מוגבלת בהוראות משפטיות אחרות מעבר לחוק זכויות היוצרים. ר' את תיאור הקובץ לפרטים.",
+ "multimediaviewer-restriction-default-and-others": "התמונה הזאת יכולה להיות מוגבלת בהוראות משפטיות אחרות מעבר לחוק זכויות היוצרים. ר' את תיאור הקובץ לפרטים.",
+ "multimediaviewer-about-mmv": "אודות",
+ "multimediaviewer-discuss-mmv": "דיון",
+ "multimediaviewer-help-mmv": "עזרה",
+ "multimediaviewer-optout-mmv": "כיבוי מציג המדיה",
+ "multimediaviewer-optin-mmv": "הפעלת מציג המדיה",
+ "multimediaviewer-optout-pending-mmv": "כיבוי מציג מדיה",
+ "multimediaviewer-optin-pending-mmv": "הפעלת מציג מדיה",
+ "multimediaviewer-optout-help": "מציג המדיה לא ישמש עוד להצגת תמונות. כדי להשתמש בו שוב, יש ללחוץ על כפתור \"{{int:multimediaviewer-view-expanded}}\" ליד תמונה. אחרי־כן יש ללחוץ על \"{{int:multimediaviewer-optin-mmv}}\".",
+ "multimediaviewer-optin-help": "מציג המדיה ישמש להצגת תמונות.",
+ "multimediaviewer-geoloc-north": "צפ",
+ "multimediaviewer-geoloc-east": "מז",
+ "multimediaviewer-geoloc-south": "דר",
+ "multimediaviewer-geoloc-west": "מע",
+ "multimediaviewer-geolocation": "מיקום: $1",
+ "multimediaviewer-reuse-link": "שיתוף או הטבעה של הקובץ הזה",
+ "multimediaviewer-reuse-loading-placeholder": "טעינה...",
+ "multimediaviewer-reuse-copy-share": "לבחור ולהעתיק (אם זה נתמך) את הקישור לשיתוף הקובץ הזה",
+ "multimediaviewer-reuse-copy-embed": "לבחור ולהעתיק (אם זה נתמך) את הקישור להטבעת הקובץ הזה",
+ "multimediaviewer-share-tab": "שיתוף",
+ "multimediaviewer-embed-tab": "הטבעה",
+ "multimediaviewer-download-link": "הורדת הקובץ הזה",
+ "multimediaviewer-download-preview-link-title": "תצוגה בדפדפן",
+ "multimediaviewer-download-original-button-name": "הורדת הקובץ המקורי",
+ "multimediaviewer-download-small-button-name": "הורדה בגודל קטן",
+ "multimediaviewer-download-medium-button-name": "הורדה בגודל בינוני",
+ "multimediaviewer-download-large-button-name": "הורדה בגודל גדול",
+ "multimediaviewer-link-to-page": "קישור לדף תיאור הקובץ",
+ "multimediaviewer-link-to-file": "קישור לקובץ המקורי",
+ "multimediaviewer-share-explanation": "קישור להעתקה ושיתוף חופשי",
+ "multimediaviewer-embed-wt": "קוד ויקי",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-embed-explanation": "קוד להטבעת הקובץ",
+ "multimediaviewer-text-embed-credit-text-bl": "מאת $1, $2, $3",
+ "multimediaviewer-text-embed-credit-text-b": "מאת $1, $2",
+ "multimediaviewer-html-embed-credit-text-bl": "מאת $1, $2, $3",
+ "multimediaviewer-html-embed-credit-text-b": "מאת $1, $2",
+ "multimediaviewer-html-embed-credit-link-text": "קישור",
+ "multimediaviewer-embed-byline": "מאת $1",
+ "multimediaviewer-embed-license": "ברישיון $1.",
+ "multimediaviewer-embed-via": "דרך $1.",
+ "multimediaviewer-default-embed-dimensions": "גודל התחלתי לתמונה ממוזערת",
+ "multimediaviewer-original-embed-dimensions": "הקובץ המקורי $1",
+ "multimediaviewer-large-embed-dimensions": "גדול $1",
+ "multimediaviewer-medium-embed-dimensions": "בינוני $1",
+ "multimediaviewer-small-embed-dimensions": "קטן $1",
+ "multimediaviewer-embed-dimensions": "$1 × $2 פיקסלים",
+ "multimediaviewer-description-page-button-text": "פרטים נוספים על הקובץ הזה",
+ "multimediaviewer-description-page-popup-text": "פרטים נוספים על הקובץ הזה ב{{GRAMMAR:תחילית|$1}}",
+ "multimediaviewer-commons-subtitle": "מאגר המדיה החופשי",
+ "multimediaviewer-view-expanded": "פתיחה במציג המדיה",
+ "multimediaviewer-view-config": "הגדרות",
+ "multimediaviewer-close-popup-text": "סגירת הכלי הזה (Esc)",
+ "multimediaviewer-fullscreen-popup-text": "הצגה במסך מלא",
+ "multimediaviewer-defullscreen-popup-text": "יציאה ממסך מלא",
+ "multimediaviewer-next-image-alt-text": "להציג את התמונה הבאה",
+ "multimediaviewer-prev-image-alt-text": "הצגת התמונה הקודמת",
+ "multimediaviewer-title-popup-text": "תיאור",
+ "multimediaviewer-credit-popup-text": "מידע על המחבר והמקור",
+ "multimediaviewer-title-popup-text-more": "הצגת תיאור מלא",
+ "multimediaviewer-credit-popup-text-more": "הצגת יוצר ומקור",
+ "multimediaviewer-download-attribution-cta-header": "חובה לתת ייחוס ליוצר",
+ "multimediaviewer-download-optional-attribution-cta-header": "באפשרותך לתת ייחוס ליוצר",
+ "multimediaviewer-download-attribution-cta": "תראו לי איך",
+ "multimediaviewer-download-attribution-copy": "נא לבחור ולהעתיק (אם אפשר) את טקסט הייחוס עבור הקובץ הזה",
+ "multimediaviewer-reuse-warning-deletion": "נשקלת מחיקה של הקובץ הזה.",
+ "multimediaviewer-reuse-warning-nonfree": "לקובץ הזה אין רישיון חופשי.",
+ "multimediaviewer-reuse-warning-noattribution": "לקובץ הזה אין מידע על ייחוס.",
+ "multimediaviewer-reuse-warning-generic": "נא לבדוק [$1 את הפרטים שלו] לפני השימוש בו.",
+ "multimediaviewer-attr-plain": "טקסט רגיל",
+ "multimediaviewer-options-tooltip": "הפעלה או כיבוי של תצוגה מקדימה",
+ "multimediaviewer-options-dialog-header": "לכבות את מציג המדיה?",
+ "multimediaviewer-options-text-header": "דילוג על הצגת התכונה הזאת לכל הקבצים.",
+ "multimediaviewer-options-text-body": "באפשרותך להפעיל את זה מאוחר יותר דרך דף הגדרות הקובץ.",
+ "multimediaviewer-options-learn-more": "מידע נוסף",
+ "multimediaviewer-option-submit-button": "כיבוי מציג המדיה",
+ "multimediaviewer-option-cancel-button": "ביטול",
+ "multimediaviewer-disable-confirmation-header": "כיבית את מציג המדיה",
+ "multimediaviewer-disable-confirmation-text": "בעת הלחיצה הבאה על תמונה ממוזערת ב{{GRAMMAR:תחילית|$1}}, יוצגו לך כל פרטי הקובץ.",
+ "multimediaviewer-enable-dialog-header": "להפעיל את מציג המדיה?",
+ "multimediaviewer-enable-text-header": "להפעיל את היכולת הזאת להציג מדיה עבור כל הקבצים לפי בררת המחדל.",
+ "multimediaviewer-enable-submit-button": "הפעלת מציג המדיה",
+ "multimediaviewer-enable-confirmation-header": "הפעלת את מציג המדיה עבור כל הקבצים",
+ "multimediaviewer-enable-confirmation-text": "בלחיצה הבאה על תמונה ממוזערת ב{{GRAMMAR:תחילית|$1}} ישמש מציג המדיה.",
+ "multimediaviewer-enable-alert": "מציג המדיה כבוי עכשיו",
+ "multimediaviewer-disable-info-title": "כיבית את מציג המדיה",
+ "multimediaviewer-disable-info": "היכולת עדיין להציג קבצים בודדים עם מציג המדיה.",
+ "multimediaviewer-errorreport-privacywarning": "פרטי השגיאה מצורפים לדו\"ח, שיהיה זמין לצפייה באופן ציבורי. אם זה לא מתאים לך, באפשרותך לערוך את הדו\"ח להלן ולמחוק את כל הנתונים שאין ברצונך לשתף."
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/hi.json b/www/wiki/extensions/MultimediaViewer/i18n/hi.json
new file mode 100644
index 00000000..44a8a741
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/hi.json
@@ -0,0 +1,25 @@
+{
+ "@metadata": {
+ "authors": [
+ "Kunalgrover05",
+ "Siddhartha Ghai",
+ "Vivek Rai",
+ "Wikiuser13",
+ "Smtchahal",
+ "Upendradutt93",
+ "Sfic"
+ ]
+ },
+ "multimediaviewer-permission-title": "अनुमति विवरण",
+ "multimediaviewer-permission-link": "शर्तें देखें",
+ "multimediaviewer-about-mmv": "के बारे में",
+ "multimediaviewer-discuss-mmv": "वार्ता",
+ "multimediaviewer-reuse-loading-placeholder": "लोड हो रहा है…",
+ "multimediaviewer-embed-tab": "एम्बेड करें",
+ "multimediaviewer-download-link": "इस फ़ाइल को डाउनलोड करें",
+ "multimediaviewer-download-small-button-name": "छोटा आकार डाउनलोड करें",
+ "multimediaviewer-download-medium-button-name": "मध्यम आकार डाउनलोड करें",
+ "multimediaviewer-link-to-file": "मूल फ़ाइल से लिंक करें",
+ "multimediaviewer-embed-byline": "$1 द्वारा",
+ "multimediaviewer-description-page-button-text": "इस फ़ाइल की अधिक जानकारी"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/hr.json b/www/wiki/extensions/MultimediaViewer/i18n/hr.json
new file mode 100644
index 00000000..7674b9ec
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/hr.json
@@ -0,0 +1,135 @@
+{
+ "@metadata": {
+ "authors": [
+ "MaGa",
+ "Bugoslav"
+ ]
+ },
+ "multimediaviewer-desc": "Sličice u većoj veličini u prikazu preko cijelog zaslona.",
+ "multimediaviewer-pref": "Preglednik medijskih datoteka",
+ "multimediaviewer-pref-desc": "Poboljšajte svoje iskustvo pregledavajući multimedijske sadržaje uz pomoć ovog alata. Alat prikazuje slike u većoj veličini i u ljepšem Lightbox okviru. Slike se mogu vidjeti i u pravoj veličini.",
+ "multimediaviewer-optin-pref": "Omogući <span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About preglednik multimedijskih datoteka]</span>",
+ "multimediaviewer-file-page": "Idi na odgovarajuću stranicu datoteke",
+ "multimediaviewer-repository-local": "Više pojedinosti",
+ "multimediaviewer-datetime-created": "Stvoreno: $1",
+ "multimediaviewer-datetime-uploaded": "Postavljeno: $1",
+ "multimediaviewer-credit-fallback": "Vidi informacije o autoru",
+ "multimediaviewer-multiple-authors": "{{PLURAL:$1|još jedan autor|još $1 autora}}",
+ "multimediaviewer-multiple-authors-combine": "$1 i $2",
+ "multimediaviewer-metadata-error": "Nije bilo moguće učitavanje pojedinosti slike (pogrješka: $1)",
+ "multimediaviewer-thumbnail-error": "Žao nam je, datoteku nije moguće prikazati",
+ "multimediaviewer-thumbnail-error-description": "Čini se da je došlo do tehničke poteškoće. Možete $1 ili $3 ako se opet pojavi. Pogrješka: $2",
+ "multimediaviewer-thumbnail-error-retry": "ponoviti",
+ "multimediaviewer-thumbnail-error-report": "prijaviti poteškoću",
+ "multimediaviewer-license-cc-by-1.0": "CC BY 1.0",
+ "multimediaviewer-license-cc-sa-1.0": "CC SA 1.0",
+ "multimediaviewer-license-cc-by-sa-1.0": "CC BY-SA 1.0",
+ "multimediaviewer-license-cc-by-2.0": "CC BY 2.0",
+ "multimediaviewer-license-cc-by-sa-2.0": "CC BY-SA 2.0",
+ "multimediaviewer-license-cc-by-2.1": "CC BY 2.1",
+ "multimediaviewer-license-cc-by-sa-2.1": "CC BY-SA 2.1",
+ "multimediaviewer-license-cc-by-2.5": "CC BY 2.5",
+ "multimediaviewer-license-cc-by-sa-2.5": "CC BY-SA 2.5",
+ "multimediaviewer-license-cc-by-3.0": "CC BY 3.0",
+ "multimediaviewer-license-cc-by-sa-3.0": "CC BY-SA 3.0",
+ "multimediaviewer-license-cc-by-4.0": "CC BY 4.0",
+ "multimediaviewer-license-cc-by-sa-4.0": "CC BY-SA 4.0",
+ "multimediaviewer-license-cc-pd": "Javno vlasništvo",
+ "multimediaviewer-license-cc-zero": "CC 0",
+ "multimediaviewer-license-pd": "Javno vlasništvo",
+ "multimediaviewer-license-default": "Prikaži licenciju",
+ "multimediaviewer-permission-title": "Pojedinosti dopuštenja",
+ "multimediaviewer-permission-link": "vidi uvjete",
+ "multimediaviewer-permission-link-hide": "prikaži uvjete",
+ "multimediaviewer-permission-viewmore": "Pogledaj više",
+ "multimediaviewer-restriction-2257": "Ova slika sadrži eksplicitne seksualne prizore koji mogu biti podložni Zakonu o zaštiti djece te kontroli neprimjerenog materijala (Child Protection and Obscenity Enforcement Act).",
+ "multimediaviewer-about-mmv": "O pregledniku multimedijskih datoteka",
+ "multimediaviewer-discuss-mmv": "Rasprava",
+ "multimediaviewer-help-mmv": "Pomoć",
+ "multimediaviewer-optout-mmv": "Onemogući preglednik multimedijskih datoteka",
+ "multimediaviewer-optin-mmv": "Omogući preglednik multimedijskih datoteka",
+ "multimediaviewer-optout-pending-mmv": "Onemogućavanje preglednika multimedijskih datoteka",
+ "multimediaviewer-optin-pending-mmv": "Omogućavanje preglednika multimedijskih datoteka",
+ "multimediaviewer-optout-help": "Neće se više koristiti preglednik multimedijskih datoteka. Da biste ga opet omogućili, kliknite na \"{{int:multimediaviewer-view-expanded}}\" pored slike, te na \"{{int:multimediaviewer-optin-mmv}}\".",
+ "multimediaviewer-optin-help": "Za pregledavanje slika će se koristiti preglednik multimedijskih datoteka.",
+ "multimediaviewer-geoloc-north": "S",
+ "multimediaviewer-geoloc-east": "I",
+ "multimediaviewer-geoloc-south": "J",
+ "multimediaviewer-geoloc-west": "Z",
+ "multimediaviewer-geoloc-coord": "$1° $2′ $3″ $4",
+ "multimediaviewer-geoloc-coords": "$1, $2",
+ "multimediaviewer-geolocation": "Lokacija: $1",
+ "multimediaviewer-reuse-link": "Podijeli ili ugradi ovu datoteku",
+ "multimediaviewer-reuse-loading-placeholder": "Učitavanje...",
+ "multimediaviewer-reuse-copy-share": "Označite i kopirajte (ako je podržano) poveznicu za dijeljene ove datoteke",
+ "multimediaviewer-reuse-copy-embed": "Označite i kopirajte (ako je podržano) kôd za ugradnju ove datoteke",
+ "multimediaviewer-share-tab": "Podijeli",
+ "multimediaviewer-embed-tab": "Ugradi",
+ "multimediaviewer-download-link": "Preuzmi ovu datoteku",
+ "multimediaviewer-download-preview-link-title": "Pogledaj u pregledniku",
+ "multimediaviewer-download-original-button-name": "Preuzmi izvornu datoteku",
+ "multimediaviewer-download-small-button-name": "Preuzmi malu sliku",
+ "multimediaviewer-download-medium-button-name": "Preuzmi srednje veliku sliku",
+ "multimediaviewer-download-large-button-name": "Preuzmi veliku sliku",
+ "multimediaviewer-link-to-page": "Poveznica na stranicu s informacijama o datoteki",
+ "multimediaviewer-link-to-file": "Poveznica na izvornu datoteku",
+ "multimediaviewer-share-explanation": "Kopirajte i slobodno dijelite poveznicu",
+ "multimediaviewer-embed-wt": "Wikitekst",
+ "multimediaviewer-embed-explanation": "Koristite ovaj kôd da biste ugradili datoteku",
+ "multimediaviewer-text-embed-credit-text-bl": "Autor $1, $2, $3",
+ "multimediaviewer-text-embed-credit-text-b": "Autor $1, $2",
+ "multimediaviewer-text-embed-credit-text-l": "$1, $2",
+ "multimediaviewer-html-embed-credit-text-bl": "Autor $1, $2, $3",
+ "multimediaviewer-html-embed-credit-text-b": "Autor $1, $2",
+ "multimediaviewer-html-embed-credit-text-l": "$1, $2",
+ "multimediaviewer-html-embed-credit-link-text": "Poveznica",
+ "multimediaviewer-embed-byline": "Autor $1",
+ "multimediaviewer-embed-license": "Licencirano pod licencijom $1.",
+ "multimediaviewer-embed-license-nonfree": "$1.",
+ "multimediaviewer-embed-via": "Preko $1.",
+ "multimediaviewer-default-embed-dimensions": "Podrazumijevana veličina minijature",
+ "multimediaviewer-original-embed-dimensions": "Izvorna datoteka $1",
+ "multimediaviewer-large-embed-dimensions": "Velika $1",
+ "multimediaviewer-medium-embed-dimensions": "Srednja $1",
+ "multimediaviewer-small-embed-dimensions": "Mala $1",
+ "multimediaviewer-embed-dimensions": "$1 × $2 piksela",
+ "multimediaviewer-embed-dimensions-separated": "- $1",
+ "multimediaviewer-description-page-button-text": "Više pojedinosti o ovoj datoteci",
+ "multimediaviewer-description-page-popup-text": "Više pojedinosti o ovoj datoteci na projektu $1",
+ "multimediaviewer-commons-subtitle": "Spremište slobodnih datoteka",
+ "multimediaviewer-view-expanded": "Otvori u multimedijskom pregledniku",
+ "multimediaviewer-view-config": "Postavke",
+ "multimediaviewer-close-popup-text": "Zatvori alat (Esc)",
+ "multimediaviewer-fullscreen-popup-text": "Prikaži preko cijelog zaslona",
+ "multimediaviewer-defullscreen-popup-text": "Izađi iz prikaza preko cijelog ekrana",
+ "multimediaviewer-next-image-alt-text": "Prikaži sljedeću sliku",
+ "multimediaviewer-prev-image-alt-text": "Prikaži prethodnu sliku",
+ "multimediaviewer-title-popup-text": "Opis",
+ "multimediaviewer-credit-popup-text": "Autor i informacije o izvoru",
+ "multimediaviewer-title-popup-text-more": "Vidi cijeli opis",
+ "multimediaviewer-credit-popup-text-more": "Vidi puni prikaz autora i izvora",
+ "multimediaviewer-download-attribution-cta-header": "Trebate navesti autora",
+ "multimediaviewer-download-optional-attribution-cta-header": "Možetet pripisati autora",
+ "multimediaviewer-download-attribution-cta": "Pokaži mi kako",
+ "multimediaviewer-download-attribution-copy": "Označite i kopirajte (ako je podržano) tekst pripisivanja autorstva ove datoteke",
+ "multimediaviewer-attr-plain": "Običan tekst",
+ "multimediaviewer-attr-html": "HTML",
+ "multimediaviewer-options-tooltip": "Omogući ili onemogući preglednik multimedijskih datoteka",
+ "multimediaviewer-options-dialog-header": "Onemogućiti preglednik multimedijskih datoteka?",
+ "multimediaviewer-options-text-header": "Onemogući ovu značajku za sve datoteke.",
+ "multimediaviewer-options-text-body": "Naknadno ju možete omogućiti na stranici s pojedinostima datoteke.",
+ "multimediaviewer-options-learn-more": "Saznajte više",
+ "multimediaviewer-option-submit-button": "Onemogući preglednik",
+ "multimediaviewer-option-cancel-button": "Odustani",
+ "multimediaviewer-disable-confirmation-header": "Onemogućili ste preglednik",
+ "multimediaviewer-disable-confirmation-text": "Sljedeći put kad kliknete na minijaturu slike na projektu $1, bit ćete preusmjereni na pojedinosti datoteke.",
+ "multimediaviewer-enable-dialog-header": "Omogućiti preglednik multimedijskih datoteka?",
+ "multimediaviewer-enable-text-header": "Omogući ovu mogućnost ubuduće kao podrazumijevanu za sve datoteke.",
+ "multimediaviewer-enable-submit-button": "Omogući preglednik",
+ "multimediaviewer-enable-confirmation-header": "Omogućili ste preglednik za sve datoteke",
+ "multimediaviewer-enable-confirmation-text": "Sljedeći put kad kliknete na minijaturu slike na projektu $1, u uporabi će biti preglednik.",
+ "multimediaviewer-enable-alert": "Preglednik multimedijskih datoteka trenutačno je onemogućen.",
+ "multimediaviewer-disable-info-title": "Onemogućili ste preglednik",
+ "multimediaviewer-disable-info": "Još uvijek možete pregledavati pojedinačne datoteke u pregledniku.",
+ "multimediaviewer-errorreport-privacywarning": "Pojedinosti pogrješke su priloženi izvješću koje će biti javno vidljivo. Ako niste suglasni s tim, možete niže urediti izvješće i ukloniti podatke koje ne želite podijeliti."
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/hsb.json b/www/wiki/extensions/MultimediaViewer/i18n/hsb.json
new file mode 100644
index 00000000..ccfbc652
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/hsb.json
@@ -0,0 +1,81 @@
+{
+ "@metadata": {
+ "authors": [
+ "J budissin",
+ "Michawiki"
+ ]
+ },
+ "multimediaviewer-desc": "Miniaturki w powjerchu połneje wobrazowki powjetšić.",
+ "multimediaviewer-pref": "Medijowy wobhladowak",
+ "multimediaviewer-pref-desc": "Polěpš swoje dožiwjenje multimedijoweho wobhladowanja z tutym nastrojom. Zwobraznja wobrazy we wjetšej wulkosći na stronach, kotrež maja miniaturki. Wobrazy pokazuja so w rjeńšim powjerchu połneje wobrazowki a hodźa so w połnej wulkosći předstajić.",
+ "multimediaviewer-file-page": "K přisłušnej datajowej stronje",
+ "multimediaviewer-repository-local": "Dalše informacije",
+ "multimediaviewer-datetime-created": "Wutworjeny $1",
+ "multimediaviewer-datetime-uploaded": "Nahraty $1",
+ "multimediaviewer-credit-fallback": "Awtorske informacije pokazać",
+ "multimediaviewer-license-cc-pd": "Powšitkownosći přistupny",
+ "multimediaviewer-license-default": "Licencu sej wobhladać",
+ "multimediaviewer-permission-link": "pokiwy k dalewužiwanju",
+ "multimediaviewer-permission-link-hide": "pokiwy schować",
+ "multimediaviewer-permission-viewmore": "wjace pokazać",
+ "multimediaviewer-about-mmv": "Wo medijowym wobhladowaku",
+ "multimediaviewer-discuss-mmv": "Wo tutej funkciji diskutować",
+ "multimediaviewer-help-mmv": "Pomoc",
+ "multimediaviewer-optout-mmv": "Medijowy wobhladowak začinić",
+ "multimediaviewer-optin-mmv": "Medijowy wobhladowak wočinić",
+ "multimediaviewer-optout-pending-mmv": "Medijowy wobhladowak so začini",
+ "multimediaviewer-optin-pending-mmv": "Medijowy wobhladowak so wočini",
+ "multimediaviewer-geolocation": "Městno: $1",
+ "multimediaviewer-reuse-link": "Tutu dataju wužiwać abo dźělić",
+ "multimediaviewer-reuse-loading-placeholder": "Začituje so...",
+ "multimediaviewer-share-tab": "Dźělić",
+ "multimediaviewer-embed-tab": "Wužiwać",
+ "multimediaviewer-download-link": "Tutu dataju sćahnyć",
+ "multimediaviewer-download-preview-link-title": "We wobhladowaku pokazać",
+ "multimediaviewer-download-original-button-name": "Originalnu dataju sćahnyć",
+ "multimediaviewer-download-small-button-name": "We małej wulkosći sćahnyć",
+ "multimediaviewer-download-medium-button-name": "W srjedźnej wulkosći sćahnyć",
+ "multimediaviewer-download-large-button-name": "We wulkej wulkosći sćahnyć",
+ "multimediaviewer-link-to-page": "Wotkaz k wopisowanskej stronje",
+ "multimediaviewer-link-to-file": "Wotkaz k originalnej dataji",
+ "multimediaviewer-share-explanation": "Wotkaz kopěrować a dźělić",
+ "multimediaviewer-embed-wt": "WikiTekst",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-embed-byline": "Wot $1",
+ "multimediaviewer-embed-license": "Licencowane pod $1.",
+ "multimediaviewer-embed-via": "Via $1.",
+ "multimediaviewer-default-embed-dimensions": "Standardna wulkosć přehladki",
+ "multimediaviewer-original-embed-dimensions": "Originalna dataja $1",
+ "multimediaviewer-large-embed-dimensions": "Wulke $1",
+ "multimediaviewer-medium-embed-dimensions": "Srjedźne $1",
+ "multimediaviewer-small-embed-dimensions": "Małe $1",
+ "multimediaviewer-description-page-button-text": "Wjace detailow wo dataji",
+ "multimediaviewer-description-page-popup-text": "Wjace detailow wo dataji na $1",
+ "multimediaviewer-commons-subtitle": "Swobodny medijowy archiw",
+ "multimediaviewer-view-expanded": "W medijowym wobhladowaku wočinić",
+ "multimediaviewer-view-config": "Konfiguracija",
+ "multimediaviewer-close-popup-text": "Medijowy wobhladowak začinić (Esc)",
+ "multimediaviewer-fullscreen-popup-text": "Na połnej wobrazowce pokazać",
+ "multimediaviewer-defullscreen-popup-text": "Połnu wobrazowku wopušćić",
+ "multimediaviewer-title-popup-text": "Wopis",
+ "multimediaviewer-credit-popup-text": "Informacije wo awtorje a žórle",
+ "multimediaviewer-title-popup-text-more": "Dospołny wopis pokazać",
+ "multimediaviewer-credit-popup-text-more": "Dospołne informacije wo awtorje a žórle pokazać",
+ "multimediaviewer-download-attribution-cta-header": "Trěbne je podać mjeno awtora.",
+ "multimediaviewer-download-optional-attribution-cta-header": "Móžne je podać mjeno awtora.",
+ "multimediaviewer-options-tooltip": "Medijowy wobhladowak wočinić abo začinić",
+ "multimediaviewer-options-dialog-header": "Medijowy wobhladowak začinić?",
+ "multimediaviewer-options-text-body": "Móžeš wočinić medijowy wobhladowak pozdźišo přez wopisowansku stronu",
+ "multimediaviewer-options-learn-more": "Dalše informacije",
+ "multimediaviewer-option-submit-button": "Medijowy wobhladowak začinić",
+ "multimediaviewer-option-cancel-button": "Přetorhnyć",
+ "multimediaviewer-disable-confirmation-header": "Sy začinił(a) medijowy wobhladowak",
+ "multimediaviewer-enable-dialog-header": "Medijowy wobhladowak wočinić?",
+ "multimediaviewer-enable-text-header": "Medijowy wobhladowak za wšitke dataje aktiwěrować.",
+ "multimediaviewer-enable-submit-button": "Medijowy wobhladowak wočinić",
+ "multimediaviewer-enable-confirmation-header": "Sy aktiwěrował(a) medijowy wobhladowak za wšitke dataje",
+ "multimediaviewer-enable-confirmation-text": "Hdyž kliknješ přichodny raz na přehladku na $1, wužiwa so medijowy wobhladowak.",
+ "multimediaviewer-enable-alert": "Medijowy wobhladowak je nětko začinjeny",
+ "multimediaviewer-disable-info-title": "Sy začinił(a) medijowy wobhladowak",
+ "multimediaviewer-disable-info": "Móžeš přeco hišće jednotliwe dataje z medijowym wobhladowakom wobhladać"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/hu.json b/www/wiki/extensions/MultimediaViewer/i18n/hu.json
new file mode 100644
index 00000000..b797ba23
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/hu.json
@@ -0,0 +1,114 @@
+{
+ "@metadata": {
+ "authors": [
+ "Tgr",
+ "Misibacsi",
+ "Tacsipacsi",
+ "Csega"
+ ]
+ },
+ "multimediaviewer-desc": "A teljes képernyőre kinagyíthatóvá teszi a beágyazott képeket",
+ "multimediaviewer-pref": "Képnézegető",
+ "multimediaviewer-pref-desc": "A multimédiás tartalmak megnézését könnyebbé tevő eszköz. Az oldalak szövegébe beágyazott bélyegképeket kattintásra nagyobb méretben jeleníti meg, és számos egyéb adatot is mutat róluk.",
+ "multimediaviewer-optin-pref": "<span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About Képnézegető]</span> engedélyezése",
+ "multimediaviewer-file-page": "Ugrás a fájl saját lapjára",
+ "multimediaviewer-repository-local": "További részletek",
+ "multimediaviewer-datetime-created": "Létrehozva: $1",
+ "multimediaviewer-datetime-uploaded": "Feltöltve: $1",
+ "multimediaviewer-credit-fallback": "Szerzőről szóló információ megtekintése",
+ "multimediaviewer-multiple-authors": "$1 további szerző",
+ "multimediaviewer-multiple-authors-combine": "$1 és $2",
+ "multimediaviewer-metadata-error": "Nem sikerült betölteni a kép adatait (hibaüzenet: $1)",
+ "multimediaviewer-thumbnail-error": "Bocsánat, a fájl nem jeleníthető meg",
+ "multimediaviewer-thumbnail-error-description": "Úgy tűnik, technikai hiba lépett fel. $1 vagy $3, ha nem szűnik meg. Hibaüzenet: $2",
+ "multimediaviewer-thumbnail-error-retry": "Újrapróbálkozhatsz",
+ "multimediaviewer-thumbnail-error-report": "jelentheted a hibát",
+ "multimediaviewer-license-cc-pd": "Közkincs",
+ "multimediaviewer-license-pd": "Közkincs",
+ "multimediaviewer-license-default": "Licenc megtekintése",
+ "multimediaviewer-permission-title": "Részletes engedély",
+ "multimediaviewer-permission-link": "feltételek mutatása",
+ "multimediaviewer-permission-link-hide": "feltételek elrejtése",
+ "multimediaviewer-permission-viewmore": "Bővebben",
+ "multimediaviewer-restriction-communist": "Ez a kép kommunista jelvényeket tartalmaz, amelyeket egyes országokban – így Magyarországon is – csak korlátozásokkal lehet használni.",
+ "multimediaviewer-restriction-costume": "Ez a kép jelmezt ábrázol, és jogi korlátozások alá eshet.",
+ "multimediaviewer-restriction-currency": "Ez a kép egy pénzegységet ábrázol, és jogi korlátozások alá eshet.",
+ "multimediaviewer-restriction-nazi": "Ez a kép náci vagy egyéb fasiszta jelvényt tartalmaz, amelynek felhasználása egyes országokban – így Magyarországon is – korlátozva lehet.",
+ "multimediaviewer-restriction-trademarked": "Ezen a képen olyan tartalom található, ami védjegyoltalom alá eshet.",
+ "multimediaviewer-about-mmv": "Névjegy",
+ "multimediaviewer-discuss-mmv": "Megbeszélés",
+ "multimediaviewer-help-mmv": "Súgó",
+ "multimediaviewer-optout-mmv": "Képnézegető letiltása",
+ "multimediaviewer-optin-mmv": "A Képnézegető engedélyezése",
+ "multimediaviewer-optout-pending-mmv": "Képnézegető letiltása folyamatban",
+ "multimediaviewer-optin-pending-mmv": "Képnézegető engedélyezése folyamatban",
+ "multimediaviewer-optout-help": "Mostantól nem a Képnézegető fogja megjeleníteni a képeket. Az újbóli bekapcsoláshoz kattints a „{{int:multimediaviewer-view-expanded}}”, majd a „{{int:multimediaviewer-optin-mmv}}” gombra.",
+ "multimediaviewer-optin-help": "A Képnézegető fogja megjeleníteni a képeket.",
+ "multimediaviewer-geolocation": "Hely: $1",
+ "multimediaviewer-reuse-link": "A fájl megosztása vagy beágyazása",
+ "multimediaviewer-reuse-loading-placeholder": "Betöltés…",
+ "multimediaviewer-reuse-copy-share": "A fájlra mutató hivatkozás kijelölése és másolása (ha támogatott)",
+ "multimediaviewer-reuse-copy-embed": "A fájl beágyazására szolgáló kód kijelölése és másolása (ha támogatott)",
+ "multimediaviewer-share-tab": "Megosztás",
+ "multimediaviewer-embed-tab": "Beágyazás",
+ "multimediaviewer-download-link": "Fájl letöltése",
+ "multimediaviewer-download-preview-link-title": "Megtekintés a böngészőben",
+ "multimediaviewer-download-original-button-name": "Eredeti fájl letöltése",
+ "multimediaviewer-download-small-button-name": "Letöltés kis méretben",
+ "multimediaviewer-download-medium-button-name": "Letöltés közepes méretben",
+ "multimediaviewer-download-large-button-name": "Letöltés nagy méretben",
+ "multimediaviewer-link-to-page": "Link a leírólapra",
+ "multimediaviewer-link-to-file": "Link az eredeti fájlra",
+ "multimediaviewer-share-explanation": "Oszd meg ezt a linket",
+ "multimediaviewer-embed-wt": "Wikiszöveg",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-embed-explanation": "Ezzel a kóddal tudod beágyazni a fájlt",
+ "multimediaviewer-text-embed-credit-text-bl": "Készítette: $1, $2, $3",
+ "multimediaviewer-text-embed-credit-text-b": "Készítette: $1, $2",
+ "multimediaviewer-html-embed-credit-text-bl": "Készítette: $1, $2, $3",
+ "multimediaviewer-html-embed-credit-text-b": "Készítette: $1, $2",
+ "multimediaviewer-html-embed-credit-link-text": "Hivatkozás",
+ "multimediaviewer-embed-byline": "Szerző: $1",
+ "multimediaviewer-embed-license": "Engedély: $1",
+ "multimediaviewer-embed-via": "Forrás: $1",
+ "multimediaviewer-default-embed-dimensions": "Alapértelmezett bélyegképméret",
+ "multimediaviewer-original-embed-dimensions": "Eredeti fájl $1",
+ "multimediaviewer-large-embed-dimensions": "Nagy méret $1",
+ "multimediaviewer-medium-embed-dimensions": "Közepes méret $1",
+ "multimediaviewer-small-embed-dimensions": "Kis méret $1",
+ "multimediaviewer-description-page-button-text": "Bővebben erről a fájlról",
+ "multimediaviewer-description-page-popup-text": "További részletek erről a fájlról itt: $1",
+ "multimediaviewer-commons-subtitle": "A szabad képgyűjtemény",
+ "multimediaviewer-view-expanded": "Megnyitás a Képnézegetőben",
+ "multimediaviewer-view-config": "Beállítások",
+ "multimediaviewer-close-popup-text": "Eszköz bezárása (Esc)",
+ "multimediaviewer-fullscreen-popup-text": "Teljes képernyős megjelenítés",
+ "multimediaviewer-defullscreen-popup-text": "Kilépés a teljes képernyős módból",
+ "multimediaviewer-next-image-alt-text": "Következő kép megjelenítése",
+ "multimediaviewer-prev-image-alt-text": "Előző kép megjelenítése",
+ "multimediaviewer-title-popup-text": "Leírás",
+ "multimediaviewer-credit-popup-text": "Szerző- és forrásinformációk",
+ "multimediaviewer-title-popup-text-more": "Teljes leírás megtekintése",
+ "multimediaviewer-credit-popup-text-more": "Teljes szerző és forrás megtekintése",
+ "multimediaviewer-download-attribution-cta-header": "Meg kell nevezned a szerzőt",
+ "multimediaviewer-download-optional-attribution-cta-header": "Megnevezheted a szerzőt",
+ "multimediaviewer-download-attribution-cta": "Mutasd meg, hogyan",
+ "multimediaviewer-reuse-warning-deletion": "Ezt a fájlt törlésre jelölték.",
+ "multimediaviewer-reuse-warning-nonfree": "Ez a fájl nem rendelkezik szabad licenccel.",
+ "multimediaviewer-reuse-warning-generic": "Ellenőrizd a [$1 részleteket], mielőtt használnád.",
+ "multimediaviewer-attr-plain": "Sima",
+ "multimediaviewer-options-tooltip": "A Képnézegető be- vagy kikapcsolása",
+ "multimediaviewer-options-dialog-header": "Letiltod a Képnézegetőt?",
+ "multimediaviewer-options-text-body": "Később a fájlleírólapon keresztül engedélyezheted.",
+ "multimediaviewer-options-learn-more": "További információk",
+ "multimediaviewer-option-submit-button": "A Képnézegető letiltása",
+ "multimediaviewer-option-cancel-button": "Mégse",
+ "multimediaviewer-disable-confirmation-header": "Letiltottad a Képnézegetőt",
+ "multimediaviewer-disable-confirmation-text": "Amikor legközelebb egy bélyegképre kattintasz a(z) $1 wikin, azonnal látni fogod az összes fájladatot.",
+ "multimediaviewer-enable-dialog-header": "Engedélyezed a Képnézegetőt?",
+ "multimediaviewer-enable-submit-button": "A Képnézegető engedélyezése",
+ "multimediaviewer-enable-confirmation-text": "Amikor legközelebb egy bélyegképre kattintasz a(z) $1 wikin, a Képnézegető fog megjelenni.",
+ "multimediaviewer-enable-alert": "A Képnézegető most le van tiltva",
+ "multimediaviewer-disable-info-title": "Letiltottad a Képnézegetőt",
+ "multimediaviewer-disable-info": "Továbbra is megtekinthetsz egyes fájlokat a Képnézegetővel."
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/hy.json b/www/wiki/extensions/MultimediaViewer/i18n/hy.json
new file mode 100644
index 00000000..4b8ddb85
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/hy.json
@@ -0,0 +1,9 @@
+{
+ "@metadata": {
+ "authors": [
+ "Kareyac"
+ ]
+ },
+ "multimediaviewer-next-image-alt-text": "Ցույց տալ հաջորդ պատկերը",
+ "multimediaviewer-prev-image-alt-text": "Ցույց տալ նախորդ պատկերը"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/ia.json b/www/wiki/extensions/MultimediaViewer/i18n/ia.json
new file mode 100644
index 00000000..8e63543a
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/ia.json
@@ -0,0 +1,10 @@
+{
+ "@metadata": {
+ "authors": [
+ "McDutchie"
+ ]
+ },
+ "multimediaviewer-optin-pref": "Activar <span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About visualisator multimedial]</span>",
+ "multimediaviewer-multiple-authors": "{{PLURAL:$1|un altere autor|$1 altere autores}}",
+ "multimediaviewer-multiple-authors-combine": "$1 e $2"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/id.json b/www/wiki/extensions/MultimediaViewer/i18n/id.json
new file mode 100644
index 00000000..bf9b3e97
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/id.json
@@ -0,0 +1,22 @@
+{
+ "@metadata": {
+ "authors": [
+ "William Surya Permana",
+ "Ahdan"
+ ]
+ },
+ "multimediaviewer-desc": "Membentangkan gambar mini dalam ukuran yang lebih desar di dalam 'kotak tipis'",
+ "multimediaviewer-pref": "Penampil Media",
+ "multimediaviewer-pref-desc": "Tingkatkan pengalaman penampilan multimedia Anda dengan alat baru ini. Penampil Media menampilkan gambar dalam ukuran yang lebih besar pada halaman yang memiliki gambar mini. Gambar ditampilkan dalam 'kotak tipis' melayang yang lebih indah, dan dapat juga ditampilkan dalam ukuran penuh.",
+ "multimediaviewer-file-page": "Pergi ke halaman berkas terkait",
+ "multimediaviewer-repository-local": "Rincian lebih lanjut",
+ "multimediaviewer-datetime-created": "Dibuat pada $1",
+ "multimediaviewer-datetime-uploaded": "Diunggah pada $1",
+ "multimediaviewer-license-cc-pd": "Domain Umum",
+ "multimediaviewer-license-default": "Lihat lisensi",
+ "multimediaviewer-about-mmv": "Tentang Penampil Media",
+ "multimediaviewer-discuss-mmv": "Tinggalkan umpan balik",
+ "multimediaviewer-reuse-link": "Bagikan atau sematkan berkas ini",
+ "multimediaviewer-download-link": "Unduh file ini",
+ "multimediaviewer-view-expanded": "Buka di Media Viewer"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/ilo.json b/www/wiki/extensions/MultimediaViewer/i18n/ilo.json
new file mode 100644
index 00000000..d9bba969
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/ilo.json
@@ -0,0 +1,71 @@
+{
+ "@metadata": {
+ "authors": [
+ "Lam-ang"
+ ]
+ },
+ "multimediaviewer-desc": "Palawaen dagiti bassit a ladawan iti dakdakkel iti napno a pangbuyaan ti interface.",
+ "multimediaviewer-pref": "Media Viewer",
+ "multimediaviewer-pref-desc": "Pasayaatem ti panagsanay a panagbuya ti nadumaduma a midia iti daytoy baro a ramit. Daytoy ket agiparang kadagiti dakdakkel a ladawan kadagiti panid nga addaan kadagiti bassit a ladawan. Dagiti ladawan ket maiparang iti nasaysayaat a tuon iti napno a pangbuyaan ti interface, ken mabalin pay a makita iti napno a kadakkel.",
+ "multimediaviewer-optin-pref": "Pakabaelan ti <span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About Media Viewer]</span>",
+ "multimediaviewer-file-page": "Mapan iti maitutop a panid ti papeles",
+ "multimediaviewer-repository-local": "Dagiti adu pay a salaysay a maipanggep iti daytoy a papeles",
+ "multimediaviewer-datetime-created": "Pinartuat: $1",
+ "multimediaviewer-datetime-uploaded": "Inkarga: $1",
+ "multimediaviewer-metadata-error": "Biddut: Saan a maikarga ti datos ti ladawan. $1",
+ "multimediaviewer-thumbnail-error": "Biddut: Saan a maikarga ti datos ti bassit a ladawan. $1",
+ "multimediaviewer-license-cc-pd": "Dominio a Publiko",
+ "multimediaviewer-license-pd": "Dominio a Publiko",
+ "multimediaviewer-license-default": "Kitaen ti lisensia",
+ "multimediaviewer-permission-title": "Dagiti salaysay ti lisensia",
+ "multimediaviewer-permission-link": "kitaen dagiti termino",
+ "multimediaviewer-permission-viewmore": "Agkita pay ti adu",
+ "multimediaviewer-about-mmv": "Maipanggep ti Media Viewer",
+ "multimediaviewer-discuss-mmv": "Pakitungtungan daytoy a langa",
+ "multimediaviewer-help-mmv": "Tulong",
+ "multimediaviewer-optout-mmv": "Ibaldado ti Media Viewer",
+ "multimediaviewer-optin-mmv": "Pakabaelan ti Media Viewer",
+ "multimediaviewer-optout-pending-mmv": "Ibaldadon ti Media Viewer",
+ "multimediaviewer-optin-pending-mmv": "Pakabaelanen ti Media Viewer",
+ "multimediaviewer-optout-help": "Ti Media Viewer ket saanton a mausar a mangipakita kadagiti ladawan. Ti mangusar manen, pinduten ti buton ti \"{{int:multimediaviewer-view-expanded}}\" a kadenna ti ania man a ladawan. Kalpasanna pinduten ti \"{{int:multimediaviewer-optin-mmv}}\".",
+ "multimediaviewer-optin-help": "Ti Media Viewer ket mausarton a mangipakita kadagiti ladawan.",
+ "multimediaviewer-geolocation": "Lokasion: $1",
+ "multimediaviewer-reuse-link": "Usaren daytoy a papeles",
+ "multimediaviewer-reuse-loading-placeholder": "Agkarkarga...",
+ "multimediaviewer-share-tab": "Makibingay",
+ "multimediaviewer-embed-tab": "Isengngat",
+ "multimediaviewer-download-link": "Agikaraga",
+ "multimediaviewer-download-preview-link-title": "Kitaen iti pagbasabasa",
+ "multimediaviewer-download-original-button-name": "Ikarga ti kasisigud a papeles",
+ "multimediaviewer-download-small-button-name": "Ikarga ti bassit a kadakkel",
+ "multimediaviewer-download-medium-button-name": "Ikarga ti kalalainganna a kadakkel",
+ "multimediaviewer-download-large-button-name": "Ikarga ti dakkel a kadakkel",
+ "multimediaviewer-link-to-page": "Isilpo iti panid ti deskripsion ti papeles",
+ "multimediaviewer-link-to-file": "Isilpo iti kasisigud a papeles",
+ "multimediaviewer-share-explanation": "Kopiaen ken nawaya a pakibingayan ti silpo",
+ "multimediaviewer-embed-wt": "Wikitext",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-embed-explanation": "Isaren daytoy a kodigo tapno maisengngat ti papeles",
+ "multimediaviewer-embed-byline": "Babaen ni $1",
+ "multimediaviewer-embed-license": "Nalisensiaan babaen ti $1.",
+ "multimediaviewer-embed-via": "Babaen ti $1.",
+ "multimediaviewer-default-embed-dimensions": "Kasisigud a kadakkel ti bassit a ladawan",
+ "multimediaviewer-original-embed-dimensions": "Kasisigud a ladawan $1",
+ "multimediaviewer-large-embed-dimensions": "Dakkel $1",
+ "multimediaviewer-medium-embed-dimensions": "Kalalainganna $1",
+ "multimediaviewer-small-embed-dimensions": "Bassit $1",
+ "multimediaviewer-description-page-button-text": "Dagiti adu pay a salaysay a maipanggep iti daytoy a papeles",
+ "multimediaviewer-description-page-popup-text": "Dagiti adu pay a salaysay a maipanggep iti daytoy a papeles idiay $1",
+ "multimediaviewer-commons-subtitle": "Ti nawaya a repositorio ti midia",
+ "multimediaviewer-view-expanded": "Padakkelen ti panagbuya",
+ "multimediaviewer-close-popup-text": "Irikep daytoy a ramit (Esc)",
+ "multimediaviewer-fullscreen-popup-text": "Ipakita ti napno a pagbuyaan",
+ "multimediaviewer-defullscreen-popup-text": "Rummuar iti napno a pagbuyaan",
+ "multimediaviewer-title-popup-text": "Nagan ti papeles",
+ "multimediaviewer-credit-popup-text": "Mannurat ken pakaammo ti taudan",
+ "multimediaviewer-title-popup-text-more": "Pinduten tapno maiparang ti napno a nagan ti papeles",
+ "multimediaviewer-credit-popup-text-more": "Pinduten tapno maipakita ti napno a mannurat ken taudan",
+ "multimediaviewer-download-attribution-cta-header": "Nasken a pammadayawam ti mannurat",
+ "multimediaviewer-download-attribution-cta": "Ipakitam kaniak no kasano",
+ "multimediaviewer-attr-plain": "Naranas"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/inh.json b/www/wiki/extensions/MultimediaViewer/i18n/inh.json
new file mode 100644
index 00000000..b6c35df5
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/inh.json
@@ -0,0 +1,9 @@
+{
+ "@metadata": {
+ "authors": [
+ "Умар",
+ "ElizaMag"
+ ]
+ },
+ "multimediaviewer-help-mmv": "Новкъoстал"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/io.json b/www/wiki/extensions/MultimediaViewer/i18n/io.json
new file mode 100644
index 00000000..d9f0b386
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/io.json
@@ -0,0 +1,10 @@
+{
+ "@metadata": {
+ "authors": [
+ "Joao Xavier"
+ ]
+ },
+ "multimediaviewer-repository-local": "Plura detali",
+ "multimediaviewer-permission-viewmore": "Montrez pluse",
+ "multimediaviewer-description-page-button-text": "Plura detali pri ca arkivo"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/is.json b/www/wiki/extensions/MultimediaViewer/i18n/is.json
new file mode 100644
index 00000000..6005486e
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/is.json
@@ -0,0 +1,82 @@
+{
+ "@metadata": {
+ "authors": [
+ "Snævar",
+ "Sveinn í Felli"
+ ]
+ },
+ "multimediaviewer-file-page": "Fara á samsvarandi skráarsíðu",
+ "multimediaviewer-repository-local": "Frekari upplýsingar",
+ "multimediaviewer-datetime-created": "Búið til: $1",
+ "multimediaviewer-datetime-uploaded": "Hlaðið inn: $1",
+ "multimediaviewer-multiple-authors-combine": "$1 og $2",
+ "multimediaviewer-metadata-error": "Gat ekki hlaðið inn myndagögnum. (villa: $1)",
+ "multimediaviewer-thumbnail-error": "Því miður var ekki hægt að sýna skránna.",
+ "multimediaviewer-thumbnail-error-retry": "Reyna aftur",
+ "multimediaviewer-license-cc-by-1.0": "CC BY 1.0",
+ "multimediaviewer-license-cc-by-sa-1.0": "CC BY-SA 1.0",
+ "multimediaviewer-license-cc-by-2.0": "CC BY 2.0",
+ "multimediaviewer-license-cc-by-sa-2.0": "CC BY-SA 2.0",
+ "multimediaviewer-license-cc-by-2.1": "CC BY 2.1",
+ "multimediaviewer-license-cc-by-sa-2.1": "CC BY-SA 2.1",
+ "multimediaviewer-license-cc-by-2.5": "CC BY 2.5",
+ "multimediaviewer-license-cc-by-sa-2.5": "CC BY-SA 2.5",
+ "multimediaviewer-license-cc-by-3.0": "CC BY 3.0",
+ "multimediaviewer-license-cc-by-sa-3.0": "CC BY-SA 3.0",
+ "multimediaviewer-license-cc-by-4.0": "CC BY 4.0",
+ "multimediaviewer-license-cc-by-sa-4.0": "CC BY-SA 4.0",
+ "multimediaviewer-license-cc-pd": "Í almenningi",
+ "multimediaviewer-license-cc-zero": "CC 0",
+ "multimediaviewer-license-pd": "Í almenningi",
+ "multimediaviewer-license-default": "Skoða leyfi",
+ "multimediaviewer-permission-link": "skoða skilmála",
+ "multimediaviewer-permission-viewmore": "sýna meira",
+ "multimediaviewer-about-mmv": "Um",
+ "multimediaviewer-discuss-mmv": "Umræða",
+ "multimediaviewer-help-mmv": "Hjálp",
+ "multimediaviewer-geoloc-north": "N",
+ "multimediaviewer-geoloc-east": "A",
+ "multimediaviewer-geoloc-south": "S",
+ "multimediaviewer-geoloc-west": "V",
+ "multimediaviewer-geolocation": "Staðsetning: $1",
+ "multimediaviewer-reuse-link": "Deila eða innifela þessa skrá",
+ "multimediaviewer-reuse-loading-placeholder": "Hleð...",
+ "multimediaviewer-share-tab": "Deila",
+ "multimediaviewer-embed-tab": "Ívefja",
+ "multimediaviewer-download-link": "Hlaða niður þessari skrá",
+ "multimediaviewer-download-preview-link-title": "Skoða í vafra",
+ "multimediaviewer-download-original-button-name": "Hlaða niður upprunalegri skrá",
+ "multimediaviewer-download-small-button-name": "Hlaða niður smámynd",
+ "multimediaviewer-download-medium-button-name": "Hlaða niður miðlungstærð",
+ "multimediaviewer-download-large-button-name": "Hlaða niður stórri stærð",
+ "multimediaviewer-link-to-file": "Tengill á upphaflega skrá",
+ "multimediaviewer-share-explanation": "Afrita og deila tenglinum frjálst",
+ "multimediaviewer-embed-wt": "Wiki-texti",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-text-embed-credit-text-bl": "Með $1, $2, $3",
+ "multimediaviewer-text-embed-credit-text-b": "Með $1, $2",
+ "multimediaviewer-html-embed-credit-text-bl": "Með $1, $2, $3",
+ "multimediaviewer-html-embed-credit-text-b": "Með $1, $2",
+ "multimediaviewer-html-embed-credit-link-text": "Tengill",
+ "multimediaviewer-embed-byline": "Eftir $1",
+ "multimediaviewer-embed-license": "Gefið út undir $1",
+ "multimediaviewer-embed-via": "Margmiðlunarskrá frá $1",
+ "multimediaviewer-default-embed-dimensions": "Sjálfgefin smámynda stærð",
+ "multimediaviewer-original-embed-dimensions": "Upphafleg skrá $1",
+ "multimediaviewer-large-embed-dimensions": "Stór $1",
+ "multimediaviewer-medium-embed-dimensions": "Miðlungs $1",
+ "multimediaviewer-small-embed-dimensions": "Lítil $1",
+ "multimediaviewer-embed-dimensions": "$1 × $2 px",
+ "multimediaviewer-description-page-button-text": "Frekari upplýsingar um þessa skrá",
+ "multimediaviewer-description-page-popup-text": "Frekari upplýsingar um þessa skrá á $1",
+ "multimediaviewer-view-config": "Uppsetning",
+ "multimediaviewer-defullscreen-popup-text": "Fara úr skjáfylliham",
+ "multimediaviewer-next-image-alt-text": "Lag",
+ "multimediaviewer-prev-image-alt-text": "Yfirlegg",
+ "multimediaviewer-title-popup-text": "Lýsing",
+ "multimediaviewer-download-attribution-cta": "Sýndu mér hvernig",
+ "multimediaviewer-attr-plain": "Hreintexti",
+ "multimediaviewer-attr-html": "HTML",
+ "multimediaviewer-options-learn-more": "Vita meira",
+ "multimediaviewer-option-cancel-button": "Hætta við"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/it.json b/www/wiki/extensions/MultimediaViewer/i18n/it.json
new file mode 100644
index 00000000..0315299d
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/it.json
@@ -0,0 +1,137 @@
+{
+ "@metadata": {
+ "authors": [
+ "Beta16",
+ "CristianCantoro",
+ "Maria victoria",
+ "OrbiliusMagister",
+ "Rosh",
+ "Melos",
+ "PeppeAeco",
+ "Toadino2",
+ "Macofe",
+ "Alexmar983",
+ "Fringio"
+ ]
+ },
+ "multimediaviewer-desc": "Espande le miniature in dimensioni maggiori in un'interfaccia a schermo intero.",
+ "multimediaviewer-pref": "Media Viewer",
+ "multimediaviewer-pref-desc": "Sperimenta una miglior visualizzazione dei file multimediali con questo nuovo strumento che visualizza le immagini più grandi su pagine che ne riportano le miniature. Le immagini sono mostrate in un'interfaccia a schermo intero più gradevole, ma possono essere visualizzate anche alla dimensione originale.",
+ "multimediaviewer-optin-pref": "Attiva <span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About Media Viewer]</span>",
+ "multimediaviewer-file-page": "Vai alla corrispondente pagina del file",
+ "multimediaviewer-repository-local": "Ulteriori dettagli",
+ "multimediaviewer-datetime-created": "Creato: $1",
+ "multimediaviewer-datetime-uploaded": "Caricato: $1",
+ "multimediaviewer-credit-fallback": "Vedi informazioni sull'autore",
+ "multimediaviewer-multiple-authors": "{{PLURAL:$1|un altro autore|altri $1 autori}}",
+ "multimediaviewer-multiple-authors-combine": "$1 e $2",
+ "multimediaviewer-metadata-error": "Impossibile caricare i dettagli dell'immagine (errore: $1)",
+ "multimediaviewer-thumbnail-error": "Spiacenti, il file non può essere visualizzato",
+ "multimediaviewer-thumbnail-error-description": "Sembra esserci un problema tecnico. Puoi $1 o $2 se persiste. Errore: $3",
+ "multimediaviewer-thumbnail-error-retry": "riprova",
+ "multimediaviewer-thumbnail-error-report": "segnalare il problema",
+ "multimediaviewer-license-cc-pd": "Pubblico dominio",
+ "multimediaviewer-license-pd": "Pubblico dominio",
+ "multimediaviewer-license-default": "Vedi la licenza",
+ "multimediaviewer-permission-title": "Dettagli dell'autorizzazione",
+ "multimediaviewer-permission-link": "vedi termini",
+ "multimediaviewer-permission-link-hide": "nascondi termini",
+ "multimediaviewer-permission-viewmore": "Mostra altro",
+ "multimediaviewer-restriction-2257": "Questa immagine contiene del contenuto sessualmente esplicito che potrebbe essere soggetto al Child Protection and Obscenity Enforcement Act negli Stati Uniti.",
+ "multimediaviewer-restriction-aus-reserve": "Questa immagine è stata fotografata in una riserva dello Stato dell'Australia e non può essere utilizzata per scopo commerciali senza autorizzazione.",
+ "multimediaviewer-restriction-communist": "Questa immagine contiene simboli comunisti che potrebbero essere vietati in alcuni paesi.",
+ "multimediaviewer-restriction-costume": "Quest'immagine rappresenta dei costumi e potrebbe essere soggetta a limitazioni legali.",
+ "multimediaviewer-restriction-currency": "Questa immagine rappresenta una raffigurazione di una unità di valuta e può essere soggetta a restrizioni legali.",
+ "multimediaviewer-restriction-design": "Il progetto del soggetto di quest'immagine potrebbe essere protetto dal diritto d'autore e potrebbe essere soggetto a limitazioni legali.",
+ "multimediaviewer-restriction-fan-art": "Quest'immagine contiene del fan art e il riutilizzo potrebbe essere soggetto a limitazioni legali.",
+ "multimediaviewer-restriction-ihl": "Questa immagine contiene simboli soggetti a restrizioni dalla International Humanitarian Law.",
+ "multimediaviewer-restriction-insignia": "Questa immagine contiene insegne ufficiali che potrebbero essere soggette a limitazioni legali.",
+ "multimediaviewer-restriction-ita-mibac": "Quest'immagine riproduce una proprietà appartenente all'eredità culturale italiana ed è limitata dalla legge italiana.",
+ "multimediaviewer-restriction-nazi": "Questa immagine contiene simboli nazisti o fascisti che potrebbero essere vietati in alcuni paesi.",
+ "multimediaviewer-restriction-personality": "Questa immagine contiene persone che potrebbero avere diritti che limitano legalmente alcuni utilizzi dell'immagine senza consenso.",
+ "multimediaviewer-restriction-trademarked": "Il contenuto di quest'immagine potrebbe essere limitato da leggi sui marchi registrati.",
+ "multimediaviewer-restriction-default": "Questa immagine potrebbe essere limitata da clausole legali fuori dalla legge sul diritto dl'autore. Maggiori dettagli nella pagina di descrizione del file.",
+ "multimediaviewer-restriction-default-and-others": "Questa immagine potrebbe essere ulteriormente limitata da altre clausole legali fuori dalla legge sul diritto d'autore. Maggiori dettagli nella pagina di descrizione del file.",
+ "multimediaviewer-about-mmv": "Informazioni",
+ "multimediaviewer-discuss-mmv": "Discussione",
+ "multimediaviewer-help-mmv": "Aiuto",
+ "multimediaviewer-optout-mmv": "Disattiva Media Viewer",
+ "multimediaviewer-optin-mmv": "Attiva Media Viewer",
+ "multimediaviewer-optout-pending-mmv": "Disattiva Media Viewer",
+ "multimediaviewer-optin-pending-mmv": "Sto attivando Media Viewer",
+ "multimediaviewer-optout-help": "Media Viewer non verrà più utilizzato per mostrare immagini. Per riusarlo, clicca sul pulsante \"{{int:multimediaviewer-view-expanded}}\" accanto ad ogni immagine. Poi clicca su \"{{int:multimediaviewer-optin-mmv}}\".",
+ "multimediaviewer-optin-help": "Sarà usato Media Viewer per mostrare le immagini.",
+ "multimediaviewer-geolocation": "Posizione: $1",
+ "multimediaviewer-reuse-link": "Condividi o incorpora questo file",
+ "multimediaviewer-reuse-loading-placeholder": "Caricamento in corso…",
+ "multimediaviewer-reuse-copy-share": "Seleziona e copia (se supportato) il collegamento per condividere questo file",
+ "multimediaviewer-reuse-copy-embed": "Seleziona e copia (se supportato) il codice per incorporare questo file",
+ "multimediaviewer-share-tab": "Condividi",
+ "multimediaviewer-embed-tab": "Incorpora",
+ "multimediaviewer-download-link": "Scarica questo file",
+ "multimediaviewer-download-preview-link-title": "Visualizza nel browser",
+ "multimediaviewer-download-original-button-name": "Scarica il file originale",
+ "multimediaviewer-download-small-button-name": "Scarica dimensioni piccole",
+ "multimediaviewer-download-medium-button-name": "Scarica dimensioni medie",
+ "multimediaviewer-download-large-button-name": "Scarica dimensioni grandi",
+ "multimediaviewer-link-to-page": "Collegamento alla pagina di descrizione del file.",
+ "multimediaviewer-link-to-file": "Collegamento al file originale.",
+ "multimediaviewer-share-explanation": "Copia e condividi liberamente il collegamento",
+ "multimediaviewer-embed-wt": "Wikitesto",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-embed-explanation": "Usa questo codice per incorporare il file",
+ "multimediaviewer-text-embed-credit-text-bl": "Di $1, $2, $3",
+ "multimediaviewer-text-embed-credit-text-b": "Di $1, $2",
+ "multimediaviewer-html-embed-credit-text-bl": "Di $1, $2, $3",
+ "multimediaviewer-html-embed-credit-text-b": "Di $1, $2",
+ "multimediaviewer-html-embed-credit-link-text": "Collegamento",
+ "multimediaviewer-embed-byline": "Di $1",
+ "multimediaviewer-embed-license": "Con licenza $1.",
+ "multimediaviewer-embed-via": "Tramite $1.",
+ "multimediaviewer-default-embed-dimensions": "Dimensioni miniatura predefinite",
+ "multimediaviewer-original-embed-dimensions": "File originale $1",
+ "multimediaviewer-large-embed-dimensions": "Grande $1",
+ "multimediaviewer-medium-embed-dimensions": "Media $1",
+ "multimediaviewer-small-embed-dimensions": "Piccola $1",
+ "multimediaviewer-description-page-button-text": "Maggiori dettagli per questo file",
+ "multimediaviewer-description-page-popup-text": "Maggiori dettagli per questo file su $1",
+ "multimediaviewer-commons-subtitle": "L'archivio di file multimediali liberi",
+ "multimediaviewer-view-expanded": "Apri in Media Viewer",
+ "multimediaviewer-view-config": "Configurazione",
+ "multimediaviewer-close-popup-text": "Chiudi questo strumento (Esc)",
+ "multimediaviewer-fullscreen-popup-text": "Visualizza a schermo intero",
+ "multimediaviewer-defullscreen-popup-text": "Esci da schermo intero",
+ "multimediaviewer-next-image-alt-text": "Mostra l'immagine successiva",
+ "multimediaviewer-prev-image-alt-text": "Mostra l'immagine precedente",
+ "multimediaviewer-title-popup-text": "Descrizione",
+ "multimediaviewer-credit-popup-text": "Informazioni su autore e fonte",
+ "multimediaviewer-title-popup-text-more": "Vedi la descrizione completa",
+ "multimediaviewer-credit-popup-text-more": "Vedi la fonte e l'autore completo",
+ "multimediaviewer-download-attribution-cta-header": "Devi attribuire l'autore",
+ "multimediaviewer-download-optional-attribution-cta-header": "Puoi attribuire l'autore",
+ "multimediaviewer-download-attribution-cta": "Fammi vedere come",
+ "multimediaviewer-download-attribution-copy": "Selezione e copia (se supportato) il testo di attribuzione di questo file",
+ "multimediaviewer-reuse-warning-deletion": "Questo file è considerato per l'eliminazione.",
+ "multimediaviewer-reuse-warning-nonfree": "Questo file non ha una licenza libera.",
+ "multimediaviewer-reuse-warning-noattribution": "Questo file non ha informazioni sull'attribuzione.",
+ "multimediaviewer-reuse-warning-generic": "Controlla i [$1 suoi dettagli] prima di utilizzarlo.",
+ "multimediaviewer-attr-plain": "Normale",
+ "multimediaviewer-options-tooltip": "Attiva o disattiva Media Viewer",
+ "multimediaviewer-options-dialog-header": "Disabilitare Media Viewer?",
+ "multimediaviewer-options-text-header": "Salta questa modalità di visualizzazione per tutti i file.",
+ "multimediaviewer-options-text-body": "Può essere abilitata più tardi dalla pagina del file.",
+ "multimediaviewer-options-learn-more": "Ulteriori informazioni",
+ "multimediaviewer-option-submit-button": "Disattiva Media Viewer",
+ "multimediaviewer-option-cancel-button": "Annulla",
+ "multimediaviewer-disable-confirmation-header": "Hai disabilitato MediaViewer",
+ "multimediaviewer-disable-confirmation-text": "La prossima volta che clicchi su un'anteprima in $1, vedrai direttamente tutti i dettagli del file.",
+ "multimediaviewer-enable-dialog-header": "Attivare Media Viewer?",
+ "multimediaviewer-enable-text-header": "Abilita questa funzionalità di visualizzazione di file multimediali per tutti i file come predefinita.",
+ "multimediaviewer-enable-submit-button": "Attiva Media Viewer",
+ "multimediaviewer-enable-confirmation-header": "Hai abilitato Media Viewer per tutti i file",
+ "multimediaviewer-enable-confirmation-text": "La prossima volta che clicchi su un'anteprima in $1, verrà usato Media Viewer.",
+ "multimediaviewer-enable-alert": "Media Viewer è ora disabilitato",
+ "multimediaviewer-disable-info-title": "Hai disabilitato Media Viewer",
+ "multimediaviewer-disable-info": "Puoi ancora vedere file individuali con Media Viewer.",
+ "multimediaviewer-errorreport-privacywarning": "I dettagli dell'errore sono allegati al report, che sarà visibile pubblicamente. Se non sei a tuo agio con ciò, puoi modificare il report qui sotto e rimuovere tutti i dati che non vuoi condividere."
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/ja.json b/www/wiki/extensions/MultimediaViewer/i18n/ja.json
new file mode 100644
index 00000000..9093b1d5
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/ja.json
@@ -0,0 +1,107 @@
+{
+ "@metadata": {
+ "authors": [
+ "Shirayuki",
+ "Whym",
+ "SkyDaisy9",
+ "Sujiniku",
+ "Otokoume"
+ ]
+ },
+ "multimediaviewer-desc": "縮小画像を全画面表示インターフェイス内に拡大表示する",
+ "multimediaviewer-pref": "メディア ビューアー",
+ "multimediaviewer-pref-desc": "この新しいツールは、マルチメディアの表示体験を改善します。縮小画像があるページで、その画像をより大きなサイズで表示します。画像は全画面表示インターフェイスのオーバーレイ内に表示され、完全なサイズで表示させることもできます。",
+ "multimediaviewer-optin-pref": "<span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About メディアビューアー]</span>を有効にする",
+ "multimediaviewer-file-page": "対応するファイル ページに移動",
+ "multimediaviewer-repository-local": "詳細",
+ "multimediaviewer-datetime-created": "作成: $1",
+ "multimediaviewer-datetime-uploaded": "アップロード: $1",
+ "multimediaviewer-credit-fallback": "著者情報を閲覧",
+ "multimediaviewer-metadata-error": "画像の詳細を読み込めませんでした。(エラー: $1)",
+ "multimediaviewer-thumbnail-error": "すみません、ファイルを表示できません",
+ "multimediaviewer-thumbnail-error-report": "問題を報告",
+ "multimediaviewer-license-cc-by-1.0": "CC 表示 1.0",
+ "multimediaviewer-license-cc-sa-1.0": "CC 継承 1.0",
+ "multimediaviewer-license-cc-by-sa-1.0": "CC 表示-継承 1.0",
+ "multimediaviewer-license-cc-by-2.0": "CC 表示 2.0",
+ "multimediaviewer-license-cc-by-sa-2.0": "CC 表示-継承 2.0",
+ "multimediaviewer-license-cc-by-2.1": "CC 表示 2.1",
+ "multimediaviewer-license-cc-by-sa-2.1": "CC 表示-継承 2.1",
+ "multimediaviewer-license-cc-by-2.5": "CC 表示 2.5",
+ "multimediaviewer-license-cc-by-sa-2.5": "CC 表示-継承 2.5",
+ "multimediaviewer-license-cc-by-3.0": "CC 表示 3.0",
+ "multimediaviewer-license-cc-by-sa-3.0": "CC 表示-継承 3.0",
+ "multimediaviewer-license-cc-by-4.0": "CC 表示 4.0",
+ "multimediaviewer-license-cc-by-sa-4.0": "CC 表示-継承 4.0",
+ "multimediaviewer-license-cc-pd": "パブリック・ドメイン",
+ "multimediaviewer-license-cc-zero": "CC 0",
+ "multimediaviewer-license-pd": "パブリック・ドメイン",
+ "multimediaviewer-license-default": "ライセンスを閲覧",
+ "multimediaviewer-permission-title": "許可の詳細",
+ "multimediaviewer-permission-link": "規約を閲覧",
+ "multimediaviewer-permission-viewmore": "続きを表示",
+ "multimediaviewer-restriction-personality": "この画像には人物が含まれており、同意なしに行われる一定の再利用を制限する権利がその人物にある可能性があります。",
+ "multimediaviewer-restriction-trademarked": "この画像には商標法の対象になる可能性のあるものが含まれています。",
+ "multimediaviewer-restriction-default": "この画像は著作権法以外の法規定により制限を受ける可能性があります。詳細についてはファイル解説ページをご覧ください。",
+ "multimediaviewer-about-mmv": "メディア ビューアーについて",
+ "multimediaviewer-discuss-mmv": "議論",
+ "multimediaviewer-help-mmv": "ヘルプ",
+ "multimediaviewer-optout-mmv": "メディアビューアーを無効にする",
+ "multimediaviewer-optin-mmv": "メディアビューアーを有効にする",
+ "multimediaviewer-optout-pending-mmv": "メディアプレーヤーを無効にしています",
+ "multimediaviewer-optin-pending-mmv": "メディアプレーヤーを有効にしています",
+ "multimediaviewer-optout-help": "今後は画像の表示にメディアビューアーを使用しません。再び使用するには、画像の隣にある「{{int:multimediaviewer-view-expanded}}」ボタンをクリックしてください。さらに「{{int:multimediaviewer-optin-mmv}}」をクリックします。",
+ "multimediaviewer-optin-help": "今後は画像の表示にメディアビューアーを使用します。",
+ "multimediaviewer-geoloc-north": "北緯",
+ "multimediaviewer-geoloc-east": "東経",
+ "multimediaviewer-geoloc-south": "南緯",
+ "multimediaviewer-geoloc-west": "西経",
+ "multimediaviewer-geoloc-coord": "$4$1度$2分$3秒",
+ "multimediaviewer-geoloc-coords": "$1 $2",
+ "multimediaviewer-geolocation": "場所: $1",
+ "multimediaviewer-reuse-link": "このファイルを共有または埋め込む",
+ "multimediaviewer-reuse-loading-placeholder": "読み込み中…",
+ "multimediaviewer-share-tab": "共有",
+ "multimediaviewer-embed-tab": "埋め込み",
+ "multimediaviewer-download-link": "このファイルをダウンロード",
+ "multimediaviewer-download-preview-link-title": "ブラウザーで閲覧",
+ "multimediaviewer-download-original-button-name": "元のファイルをダウンロード",
+ "multimediaviewer-link-to-page": "ファイル解説ページへのリンク",
+ "multimediaviewer-link-to-file": "元ファイルへのリンク",
+ "multimediaviewer-embed-wt": "ウィキテキスト",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-embed-license": "$1 でライセンスされています。",
+ "multimediaviewer-embed-via": "$1 で発表。",
+ "multimediaviewer-default-embed-dimensions": "標準サイズのサムネール",
+ "multimediaviewer-original-embed-dimensions": "元のファイル $1",
+ "multimediaviewer-large-embed-dimensions": "大 $1",
+ "multimediaviewer-medium-embed-dimensions": "中 $1",
+ "multimediaviewer-small-embed-dimensions": "小 $1",
+ "multimediaviewer-embed-dimensions": "$1 × $2 ピクセル",
+ "multimediaviewer-description-page-button-text": "このファイルについての詳細を見る",
+ "multimediaviewer-description-page-popup-text": "このファイルについての詳細を$1で見る",
+ "multimediaviewer-commons-subtitle": "フリーなマルチメディアの保管庫",
+ "multimediaviewer-view-expanded": "メディアビューアーで開く",
+ "multimediaviewer-view-config": "配列図",
+ "multimediaviewer-close-popup-text": "このツールを閉じる (Esc)",
+ "multimediaviewer-fullscreen-popup-text": "全画面で表示",
+ "multimediaviewer-defullscreen-popup-text": "全画面表示を終了",
+ "multimediaviewer-title-popup-text": "説明",
+ "multimediaviewer-options-tooltip": "メディアビューアーの有効/無効を切り替える",
+ "multimediaviewer-options-dialog-header": "メディアビューアーを無効にしますか?",
+ "multimediaviewer-options-text-header": "全てのファイルでこの表示機能をスキップします。",
+ "multimediaviewer-options-text-body": "後にファイルの詳細のページからそれを有効にすることができます。",
+ "multimediaviewer-options-learn-more": "詳細",
+ "multimediaviewer-option-submit-button": "メディアビューアーを無効にする",
+ "multimediaviewer-option-cancel-button": "中止",
+ "multimediaviewer-disable-confirmation-header": "メディアビューアーは無効になりました。",
+ "multimediaviewer-disable-confirmation-text": "次回$1のサムネイルをクリックすると、全てのファイルの詳細を直接閲覧できます。",
+ "multimediaviewer-enable-dialog-header": "メディアビューアーを有効にしますか?",
+ "multimediaviewer-enable-text-header": "初期化によりこのメディアの表示機能を全てのファイルで有効にします。",
+ "multimediaviewer-enable-submit-button": " メディアビューアーを有効にする",
+ "multimediaviewer-enable-confirmation-header": "メディアビューアーはすべてのファイルについて有効になりました。",
+ "multimediaviewer-enable-confirmation-text": "次回サムネイルの$1をクリックし, メディアビューアーを使用します。",
+ "multimediaviewer-enable-alert": "メディアビューアーは現在使用できません。",
+ "multimediaviewer-disable-info-title": "メディアビューアーを無効にしました",
+ "multimediaviewer-disable-info": "各ファイルをメディアビューアーで閲覧することもできます。"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/jv.json b/www/wiki/extensions/MultimediaViewer/i18n/jv.json
new file mode 100644
index 00000000..5b8ccb07
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/jv.json
@@ -0,0 +1,36 @@
+{
+ "@metadata": {
+ "authors": [
+ "NoiX180"
+ ]
+ },
+ "multimediaviewer-pref": "Panontonan Médhia",
+ "multimediaviewer-optin-pref": "Urubaké <span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About Panontonan Médhia]</span>",
+ "multimediaviewer-file-page": "Menyang kaca barkas sing magepokan",
+ "multimediaviewer-repository-local": "Rerincèn liyané",
+ "multimediaviewer-datetime-created": "Digawé: $1",
+ "multimediaviewer-datetime-uploaded": "Diunggah: $1",
+ "multimediaviewer-credit-fallback": "Deleng katerangan pangripta",
+ "multimediaviewer-multiple-authors": "{{PLURAL:$1|pangripta siji liyané|pangripta $1 liyané}}",
+ "multimediaviewer-multiple-authors-combine": "$1 lan $2",
+ "multimediaviewer-metadata-error": "Ora bisa ngamot rerincèn gambar (masalah: $1)",
+ "multimediaviewer-thumbnail-error": "Nyuwun pangapura, barkasé ora bisa ditonton",
+ "multimediaviewer-thumbnail-error-description": "Kayané ana masalah tèhnis. Panjenengan bisa $1 utawa $3 yèn masalahé isih ana. Masalah: $2",
+ "multimediaviewer-thumbnail-error-retry": "jajal manèh",
+ "multimediaviewer-thumbnail-error-report": "lapuraké masalahé",
+ "multimediaviewer-license-cc-pd": "Kukuban Umum",
+ "multimediaviewer-license-pd": "Kukuban Umum",
+ "multimediaviewer-license-default": "Deleng lisènsi",
+ "multimediaviewer-permission-title": "Rerincèn bab palilah",
+ "multimediaviewer-permission-link": "deleng paugeran",
+ "multimediaviewer-permission-link-hide": "dhelikaké paugeran",
+ "multimediaviewer-permission-viewmore": "Tuduhaké liyané",
+ "multimediaviewer-about-mmv": "Ngenani",
+ "multimediaviewer-discuss-mmv": "Parembugan",
+ "multimediaviewer-help-mmv": "Pitulung",
+ "multimediaviewer-optout-mmv": "Patèni Panontonan Médhia",
+ "multimediaviewer-optin-mmv": "Urubaké Panontonan Médhia",
+ "multimediaviewer-optout-pending-mmv": "Matèni Panontonan Médhia",
+ "multimediaviewer-optin-pending-mmv": "Ngurubaké Panontonan Médhia",
+ "multimediaviewer-view-expanded": "Bukak ing Panontonan Médhia"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/ka.json b/www/wiki/extensions/MultimediaViewer/i18n/ka.json
new file mode 100644
index 00000000..15782e83
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/ka.json
@@ -0,0 +1,58 @@
+{
+ "@metadata": {
+ "authors": [
+ "Otogi",
+ "David1010",
+ "MIKHEIL",
+ "Chavch"
+ ]
+ },
+ "multimediaviewer-pref": "მედია დამთვალიერებელი",
+ "multimediaviewer-optin-pref": "<span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About მედია-მაყურებლის]</span> ჩართვა",
+ "multimediaviewer-repository-local": "დამატებითი ინფორმაცია",
+ "multimediaviewer-datetime-created": "შეიქმნა: $1",
+ "multimediaviewer-datetime-uploaded": "ატვირთულია: $1",
+ "multimediaviewer-credit-fallback": "ავტორის ინფოს ნახვა",
+ "multimediaviewer-metadata-error": "სურათის ჩატვირთვა ვერ მოხერხდა (შეცდომა: $1)",
+ "multimediaviewer-thumbnail-error": "სამწუხაროდ, ამ ფაილის ჩვენება შეუძლებელია",
+ "multimediaviewer-thumbnail-error-retry": "ხელახლა ცდა",
+ "multimediaviewer-license-cc-pd": "საზოგადოებრივი საკუთრება",
+ "multimediaviewer-license-pd": "საზოგადოებრივი საკუთრება",
+ "multimediaviewer-license-default": "ლიცენზიის ხილვა",
+ "multimediaviewer-permission-title": "ნებართვის დეტალები",
+ "multimediaviewer-permission-link": "პირობების ხილვა",
+ "multimediaviewer-permission-link-hide": "პირობების დამალვა",
+ "multimediaviewer-permission-viewmore": "იხილეთ მეტი",
+ "multimediaviewer-discuss-mmv": "მოცემული გვერდის განხილვა",
+ "multimediaviewer-help-mmv": "დახმარება",
+ "multimediaviewer-geolocation": "მდებარეობა: $1",
+ "multimediaviewer-reuse-loading-placeholder": "იტვირთება...",
+ "multimediaviewer-share-tab": "გაზიარება",
+ "multimediaviewer-embed-tab": "ჩასმა",
+ "multimediaviewer-download-link": "ამ ფაილის ჩამოტვირთვა",
+ "multimediaviewer-download-preview-link-title": "ბრაუზერში გახსნა",
+ "multimediaviewer-download-original-button-name": "თავდაპირველი ფაილის ჩამოტვირთვა",
+ "multimediaviewer-download-small-button-name": "მცირე ზომის ჩამოტვირთვა",
+ "multimediaviewer-download-medium-button-name": "საშუალო ზომის ჩამოტვირთვა",
+ "multimediaviewer-download-large-button-name": "დიდი ზომის ჩამოტვირთვა",
+ "multimediaviewer-link-to-page": "ფაილი აღწერის გვერდის ბმული",
+ "multimediaviewer-link-to-file": "თავდაპირველი ფაილის ბმული",
+ "multimediaviewer-embed-wt": "ვიკიტექსტი",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-large-embed-dimensions": "დიდი $1",
+ "multimediaviewer-medium-embed-dimensions": "საშუალო $1",
+ "multimediaviewer-small-embed-dimensions": "პატარა $1",
+ "multimediaviewer-view-config": "კონფიგურაცია",
+ "multimediaviewer-close-popup-text": "ამ ხელსაწყოს დახურვა (Esc)",
+ "multimediaviewer-fullscreen-popup-text": "მთელ ეკრანზე ჩვენება",
+ "multimediaviewer-defullscreen-popup-text": "მთლიანი ეკრანიდან გამოსვლა",
+ "multimediaviewer-title-popup-text": "აღწერა",
+ "multimediaviewer-credit-popup-text": "ავტორი და წყაროს ინფორმაცია",
+ "multimediaviewer-title-popup-text-more": "სრული აღწერის ხილვა",
+ "multimediaviewer-credit-popup-text-more": "ავტორისა და წყაროს სრულად ხილვა",
+ "multimediaviewer-download-optional-attribution-cta-header": "შეგიძლიათ მიუთითოთ ავტორი",
+ "multimediaviewer-download-attribution-cta": "მაჩვენეთ, როგორ",
+ "multimediaviewer-attr-plain": "მარტივი",
+ "multimediaviewer-options-learn-more": "გაიგეთ მეტი",
+ "multimediaviewer-option-cancel-button": "გაუქმება"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/kk-cyrl.json b/www/wiki/extensions/MultimediaViewer/i18n/kk-cyrl.json
new file mode 100644
index 00000000..a66ec807
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/kk-cyrl.json
@@ -0,0 +1,91 @@
+{
+ "@metadata": {
+ "authors": [
+ "Arystanbek",
+ "Габдулгани НИШ ХБН"
+ ]
+ },
+ "multimediaviewer-desc": "Толық экранды интерфейсте шағын суреттерді өлшемін үлкенірек етіп кеңейту",
+ "multimediaviewer-pref": "Медиа қарап шығу құралы",
+ "multimediaviewer-pref-desc": "Өзіңіздің мультимедиа көру мүмкіндігіңізді мына жаңа құралмен жетілдіріп көріңіз. Ол шағын суреті (thumbnail) бар беттердегі суреттерді үлкенірек өлшемде көрсетеді.",
+ "multimediaviewer-optin-pref": "<span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About Медиа қараушыны]</span> қосу",
+ "multimediaviewer-file-page": "Тиісті файл бетіне өту",
+ "multimediaviewer-repository-local": "Көбірек егжей-тегжейі",
+ "multimediaviewer-datetime-created": "Құрылған кезі: $1",
+ "multimediaviewer-datetime-uploaded": "Жүктелген кезі: $1",
+ "multimediaviewer-credit-fallback": "Авторы туралы мәліметті қарау",
+ "multimediaviewer-multiple-authors": "{{PLURAL:$1|бірден көп авторы|$1 автордан көбірек}}",
+ "multimediaviewer-multiple-authors-combine": "$1 және $2",
+ "multimediaviewer-metadata-error": "Сурет деректері жүктелмеді (қате: $1)",
+ "multimediaviewer-thumbnail-error": "Кешіріңіз, файл көрсетілмеді",
+ "multimediaviewer-thumbnail-error-retry": "Қайталау",
+ "multimediaviewer-license-cc-pd": "Қоғамдық қазына",
+ "multimediaviewer-license-pd": "Қоғамдық қазына",
+ "multimediaviewer-license-default": "Лицензиясын қарау",
+ "multimediaviewer-permission-title": "Рұқсат егжей-тегжейі",
+ "multimediaviewer-permission-link": "шарттарын қарау",
+ "multimediaviewer-permission-link-hide": "Шарттарды жасыру",
+ "multimediaviewer-permission-viewmore": "Көбірек қарау",
+ "multimediaviewer-about-mmv": "Медиа қарап шығу құралы туралы",
+ "multimediaviewer-discuss-mmv": "Бұл мүмкіндікті талқылау",
+ "multimediaviewer-help-mmv": "Анықтама",
+ "multimediaviewer-optout-mmv": "Медиа қараушыны сөндіру",
+ "multimediaviewer-optin-mmv": "Медиа қараушыны қосу",
+ "multimediaviewer-optout-pending-mmv": "Медиа қараушы өшірілуде",
+ "multimediaviewer-optin-pending-mmv": "Медиа қараушы қосылуда",
+ "multimediaviewer-optin-help": "Медиа қараушыны суреттерді көрсетуге қолданылады.",
+ "multimediaviewer-geolocation": "Мекені: $1",
+ "multimediaviewer-reuse-link": "Бұл файлды бөлісу және ендіру",
+ "multimediaviewer-reuse-loading-placeholder": "Жүктелуде...",
+ "multimediaviewer-share-tab": "Бөлісу",
+ "multimediaviewer-embed-tab": "Ендіру",
+ "multimediaviewer-download-link": "Бұл файлды түсіріп алу",
+ "multimediaviewer-download-preview-link-title": "Броузермен қарау",
+ "multimediaviewer-download-original-button-name": "Түпнұсқа файлды түсіріп алу",
+ "multimediaviewer-download-small-button-name": "Кішкене өлшемін түсіріп алу",
+ "multimediaviewer-download-medium-button-name": "Орташа өлшемін түсіріп алу",
+ "multimediaviewer-download-large-button-name": "Үлкен өлшемін түсіріп алу",
+ "multimediaviewer-link-to-page": "Файл сипаттама бетіне сілтеу",
+ "multimediaviewer-link-to-file": "Тұпнұсқа файлға сілтеу",
+ "multimediaviewer-share-explanation": "Сілтемені копиялау және бөлісу",
+ "multimediaviewer-embed-wt": "Уикимәтін",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-embed-explanation": "Енгізілген файлға бұл кодты қолдану",
+ "multimediaviewer-embed-byline": "$1 арқылы",
+ "multimediaviewer-embed-license": "$1 лицензиясы аясында",
+ "multimediaviewer-embed-via": "$1 арқылы",
+ "multimediaviewer-default-embed-dimensions": "Әдепкі нобай өлшемі",
+ "multimediaviewer-original-embed-dimensions": "Бастапқы файл $1",
+ "multimediaviewer-large-embed-dimensions": "Үлкен $1",
+ "multimediaviewer-medium-embed-dimensions": "Орташа $1",
+ "multimediaviewer-small-embed-dimensions": "Кіші $1",
+ "multimediaviewer-embed-dimensions": "$1 × $2 px",
+ "multimediaviewer-description-page-button-text": "Бұл файл туралы толығырақ",
+ "multimediaviewer-description-page-popup-text": "$1 жобасынан көбірек егжей-тегжейін көру",
+ "multimediaviewer-commons-subtitle": "Ашық медиа қоры",
+ "multimediaviewer-view-expanded": "Кеңейтіп көру",
+ "multimediaviewer-view-config": "Құрылымы",
+ "multimediaviewer-close-popup-text": "Бұл құралды жабу (Esc)",
+ "multimediaviewer-fullscreen-popup-text": "Толық экранда көру",
+ "multimediaviewer-defullscreen-popup-text": "Толық экраннан шығу",
+ "multimediaviewer-title-popup-text": "Сипаттамасы",
+ "multimediaviewer-credit-popup-text": "Автор және қайнар мәліметі",
+ "multimediaviewer-title-popup-text-more": "Толық сипаттамасын қарау",
+ "multimediaviewer-credit-popup-text-more": "Авторын және қайнарын толық қарау",
+ "multimediaviewer-download-optional-attribution-cta-header": "Авторын анықтай аласыз",
+ "multimediaviewer-download-attribution-cta": "Қалай екенін маған көрсет",
+ "multimediaviewer-attr-plain": "Жәй мәтін",
+ "multimediaviewer-options-tooltip": "Медиа қараушыны қосу не өшіру",
+ "multimediaviewer-options-dialog-header": "Медиа қараушыны өшіресіз бе?",
+ "multimediaviewer-options-text-header": "Барлық файлдар үшін бұл қарапшығу функциясын өшіру",
+ "multimediaviewer-options-learn-more": "Көбірек білу",
+ "multimediaviewer-option-submit-button": "Медиа қараушыны сөндіру",
+ "multimediaviewer-option-cancel-button": "Қажет емес",
+ "multimediaviewer-disable-confirmation-header": "Медиа қараушыны өшірдіңіз",
+ "multimediaviewer-enable-dialog-header": "Медиа қараушыны іске қосасыз ба?",
+ "multimediaviewer-enable-submit-button": "Медиа қараушыны қосу",
+ "multimediaviewer-enable-confirmation-header": "Барлық файлдар үшін Медиа қараушыны іске қосқансыз",
+ "multimediaviewer-enable-alert": "Медиа қараушы қаізр өшірілген",
+ "multimediaviewer-disable-info-title": "Медиа қараушыны өшіргенсіз",
+ "multimediaviewer-disable-info": "Медиа қарап шығушымен жеке файлдарды қарай аласыз"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/km.json b/www/wiki/extensions/MultimediaViewer/i18n/km.json
new file mode 100644
index 00000000..1c26fd73
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/km.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "វ័ណថារិទ្ធ"
+ ]
+ },
+ "multimediaviewer-repository-local": "ស្វែងយល់បន្ថែម"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/kn.json b/www/wiki/extensions/MultimediaViewer/i18n/kn.json
new file mode 100644
index 00000000..84d4d358
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/kn.json
@@ -0,0 +1,49 @@
+{
+ "@metadata": {
+ "authors": [
+ "Vikassy",
+ "Pavanaja",
+ "Nayvik"
+ ]
+ },
+ "multimediaviewer-desc": "ಚಿಕ್ಕ ಚಿತ್ರವನ್ನು ಪೂರ್ತಿ ಪರದೆಗೆ ದೊಡ್ಡದು ಮಾಡಿ",
+ "multimediaviewer-pref": "ಮೀಡಿಯಾ ವ್ಯೂವರ್",
+ "multimediaviewer-pref-desc": "ನಿಮ್ಮ ಬಹುಮಾಧ‍್ಯಮ ವೀಕ್ಷಣೆಯ ಅನುಭವವನ್ನು ಈ ಸಾಧನ ಬಳಸಿ ಸುಧಾರಿಸಿಕೊಳ್ಳಿ. ಚಿಕ್ಕ ಚಿತ್ರಗಳಿರುವ ಪುಟಗಳಲ್ಲಿಯ ಚಿತ್ರಗಳನ್ನು ಇದು ದೊಡ್ಡದು ಮಾಡಿ ತೋರಿಸುತ್ತದೆ. ಚಿತ್ರಗಳನ್ನು ಪೂರ್ಣಪರದೆಯಲ್ಲಿ ತೋರಿಸುತ್ತದೆ.",
+ "multimediaviewer-optin-pref": "ಬಹುಮಾಧ್ಯಮ ವೀಕ್ಷಣೆಯ ಹೊಸ ಅನುಭವವನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಿ",
+ "multimediaviewer-file-page": "ಸಂಬಂಧಪಟ್ಟ ಫೈಲ್ ಪುಟಕ್ಕೆ ಹೋಗಿ",
+ "multimediaviewer-repository-local": "ಇನ್ನಷ್ಟು ತಿಳಿಯಿರಿ",
+ "multimediaviewer-datetime-created": "$1 ರಂದು ತಯಾರಿಸಿದ್ದು",
+ "multimediaviewer-datetime-uploaded": "$1 ರಂದು ಸೇರಿಸಿದ್ದು",
+ "multimediaviewer-metadata-error": "$1 ದೋಷ: ಚಿತ್ರದ ಮಾಹಿತಿಯನ್ನು ತೆರೆಯಲು ಆಗಲಿಲ್ಲ",
+ "multimediaviewer-thumbnail-error": "$1 ದೋಷ: ಚಿಕ್ಕಚಿತ್ರ ಸೇರಿಸಲು ಆಗಲಿಲ್ಲ",
+ "multimediaviewer-license-cc-pd": "ಸಾರ್ವಜನಿಕರಿಗೆ ಸೇರಿದ್ದು",
+ "multimediaviewer-license-pd": "ಸಾರ್ವಜನಿಕರಿಗೆ ಸೇರಿದ್ದು",
+ "multimediaviewer-license-default": "ಪರವಾನಗಿ ವೀಕ್ಷಿಸಿ",
+ "multimediaviewer-permission-title": "ಪರವಾನಗಿಯ ವಿವರಗಳು",
+ "multimediaviewer-permission-link": "ಶರತ್ತುಗಳನ್ನು ವೀಕ್ಷಿಸಿ",
+ "multimediaviewer-permission-viewmore": "ಹೆಚ್ಚಿನದ್ದನ್ನು ವೀಕ್ಷಿಸಿ",
+ "multimediaviewer-about-mmv": "ಮೀಡಿಯಾ ವ್ಯೂವರ್ ಬಗ್ಗೆ",
+ "multimediaviewer-discuss-mmv": "ಈ ಗುಣವೈಶಿಷ್ಟ್ಯವನ್ನು ಚರ್ಚಿಸಿ",
+ "multimediaviewer-help-mmv": "ಸಹಾಯ",
+ "multimediaviewer-geolocation": "ಸ್ಥಾನ: $1",
+ "multimediaviewer-reuse-link": "ಈ ಫೈಲನ್ನು ಬಳಸಿ",
+ "multimediaviewer-reuse-loading-placeholder": "ಉತ್ಪೂರಿತವಾಗುತ್ತಿದೆ",
+ "multimediaviewer-share-tab": "ಹಂಚಿ",
+ "multimediaviewer-embed-tab": "ಅಂತರ್ಗತಿಸಿ",
+ "multimediaviewer-download-link": "ಡೌನ್‍ಲೋಡ್",
+ "multimediaviewer-download-preview-link-title": "ಬ್ರೌಸರ್‍ನಲ್ಲಿ ಪೂರ್ವಾವಲೋಕನ",
+ "multimediaviewer-download-original-button-name": "ಮೂಲಗಾತ್ರದಲ್ಲಿ ಡೌನ್‍ಲೋಡ್ ಮಾಡಿ",
+ "multimediaviewer-download-small-button-name": "ಚಿಕ್ಕಗಾತ್ರದಲ್ಲಿ ಡೌನ್‍ಲೋಡ್ ಮಾಡಿ",
+ "multimediaviewer-download-medium-button-name": "ಮಧ್ಯಮ ಗಾತ್ರದಲ್ಲಿ ಡೌನ್‍ಲೋಡ್ ಮಾಡಿ",
+ "multimediaviewer-download-large-button-name": "ದೊಡ್ಡಗಾತ್ರದಲ್ಲಿ ಡೌನ್‍ಲೋಡ್ ಮಾಡಿ",
+ "multimediaviewer-link-to-page": "ಫೈಲ್ ವಿವರಣೆ ಪುಟಕ್ಕೆ ಕೊಂಡಿ",
+ "multimediaviewer-link-to-file": "ಮೂಲಫೈಲ್‍ಗೆ ಕೊಂಡಿ",
+ "multimediaviewer-share-explanation": "ಕೊಂಡಿಯನ್ನು ನಕಲಿಸಿ ಮತ್ತು ಎಲ್ಲರಿಗೂ ಹಂಚಿ",
+ "multimediaviewer-embed-wt": "ವಿಕಿಪಠ್ಯ",
+ "multimediaviewer-embed-html": "ಎಚ್‍ಟಿಎಂಎಲ್",
+ "multimediaviewer-embed-explanation": "ಫೈಲನ್ನು ಅಂತರ್ಗತಿಸಲು ಈ ಕೋಡ್ ಅನ್ನು ಬಳಸಿ",
+ "multimediaviewer-description-page-button-text": "ಹೆಚ್ಚಿನ ವಿವರಗಳು",
+ "multimediaviewer-description-page-popup-text": "ಹೆಚ್ಚಿನ ವಿವರಗಳು $1 ರಲ್ಲಿ",
+ "multimediaviewer-commons-subtitle": "ಉಚಿತ ಮಾಧ್ಯಮ ಕಣಜ",
+ "multimediaviewer-view-expanded": "ಹಿಗ್ಗಿದ ವೀಕ್ಷಣೆ"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/ko.json b/www/wiki/extensions/MultimediaViewer/i18n/ko.json
new file mode 100644
index 00000000..3303f999
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/ko.json
@@ -0,0 +1,77 @@
+{
+ "@metadata": {
+ "authors": [
+ "Freebiekr",
+ "Hym411",
+ "Jskang",
+ "Priviet",
+ "아라",
+ "Keysuck",
+ "Revi",
+ "IRTC1015",
+ "Hwangjy9",
+ "Ykhwong",
+ "Jerrykim306"
+ ]
+ },
+ "multimediaviewer-desc": "섬네일을 전체 화면 인터페이스에서 더 큰 크기로 확장합니다.",
+ "multimediaviewer-pref": "미디어 뷰어",
+ "multimediaviewer-pref-desc": "이 새 도구로 멀티미디어를 보는 경험을 개선하세요. 이 도구는 섬네일이 있는 문서에서 그림을 더 크게 보여줍니다. 그림은 화면 위에 보기 편리한 전체 화면 인터페이스 안에 표시되며 전체 크기로 볼 수도 있습니다.",
+ "multimediaviewer-optin-pref": "<span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About 미디어 뷰어]</span> 사용",
+ "multimediaviewer-file-page": "해당 파일 문서로 가기",
+ "multimediaviewer-repository-local": "자세한 내용",
+ "multimediaviewer-datetime-created": "만듦: $1",
+ "multimediaviewer-datetime-uploaded": "올림: $1",
+ "multimediaviewer-metadata-error": "오류: 그림 데이터를 불러오지 못했습니다. $1",
+ "multimediaviewer-thumbnail-error": "죄송합니다. 이 파일을 표시할 수 없습니다.",
+ "multimediaviewer-thumbnail-error-description": "기술적 문제인 것 같습니다. 문제가 지속되는 경우 $1하거나 $3할 수 있습니다. 오류: $2",
+ "multimediaviewer-license-cc-pd": "퍼블릭 도메인",
+ "multimediaviewer-license-pd": "퍼블릭 도메인",
+ "multimediaviewer-license-default": "라이선스 보기",
+ "multimediaviewer-permission-title": "이용허락 세부 사항",
+ "multimediaviewer-permission-link": "이용 약관 보기",
+ "multimediaviewer-permission-viewmore": "더 보기",
+ "multimediaviewer-about-mmv": "정보",
+ "multimediaviewer-discuss-mmv": "토론",
+ "multimediaviewer-help-mmv": "도움말",
+ "multimediaviewer-geoloc-north": "북",
+ "multimediaviewer-geoloc-east": "동",
+ "multimediaviewer-geoloc-south": "남",
+ "multimediaviewer-geoloc-west": "서",
+ "multimediaviewer-geolocation": "위치: $1",
+ "multimediaviewer-reuse-link": "이 파일 공유 또는 삽입",
+ "multimediaviewer-reuse-loading-placeholder": "불러오는 중…",
+ "multimediaviewer-share-tab": "공유",
+ "multimediaviewer-embed-tab": "포함하기",
+ "multimediaviewer-download-link": "이 파일을 다운로드",
+ "multimediaviewer-download-preview-link-title": "브라우저에서 보기",
+ "multimediaviewer-download-original-button-name": "원본 파일 다운로드",
+ "multimediaviewer-download-small-button-name": "작은 크기 다운로드",
+ "multimediaviewer-download-medium-button-name": "중간 크기 다운로드",
+ "multimediaviewer-download-large-button-name": "큰 크기 다운로드",
+ "multimediaviewer-link-to-page": "파일 설명 문서로 연결",
+ "multimediaviewer-link-to-file": "원본 파일로 연결",
+ "multimediaviewer-share-explanation": "링크를 복사하고 자유롭게 공유하기",
+ "multimediaviewer-embed-wt": "위키텍스트",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-embed-explanation": "파일을 포함하려면 이 코드 사용",
+ "multimediaviewer-html-embed-credit-link-text": "링크",
+ "multimediaviewer-embed-byline": "$1 만듦",
+ "multimediaviewer-embed-license": "$1로 라이선스됨.",
+ "multimediaviewer-embed-via": "$1을(를) 통해.",
+ "multimediaviewer-default-embed-dimensions": "기본 섬네일 크기",
+ "multimediaviewer-original-embed-dimensions": "원본 파일 $1",
+ "multimediaviewer-large-embed-dimensions": "큰 크기 $1",
+ "multimediaviewer-medium-embed-dimensions": "중간 크기 $1",
+ "multimediaviewer-small-embed-dimensions": "작은 크기 $1",
+ "multimediaviewer-description-page-button-text": "이 파일의 자세한 내용",
+ "multimediaviewer-description-page-popup-text": "$1의 자세한 내용",
+ "multimediaviewer-commons-subtitle": "자유 미디어 저장소",
+ "multimediaviewer-view-expanded": "미디어 뷰어에서 열기",
+ "multimediaviewer-next-image-alt-text": "다음 사진 보이기",
+ "multimediaviewer-prev-image-alt-text": "이전 사진 보이기",
+ "multimediaviewer-reuse-warning-deletion": "이 파일은 삭제를 고려하고 있습니다.",
+ "multimediaviewer-reuse-warning-nonfree": "이 파일은 자유 라이선스가 아닙니다.",
+ "multimediaviewer-reuse-warning-noattribution": "이 파일에 속성 정보가 없습니다.",
+ "multimediaviewer-reuse-warning-generic": "사용하기 전에 [$1 상세 내용]을 확인하십시오."
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/krc.json b/www/wiki/extensions/MultimediaViewer/i18n/krc.json
new file mode 100644
index 00000000..f10b3f21
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/krc.json
@@ -0,0 +1,17 @@
+{
+ "@metadata": {
+ "authors": [
+ "Iltever",
+ "Ernác"
+ ]
+ },
+ "multimediaviewer-pref": "Медиа-къаратыучу",
+ "multimediaviewer-pref-desc": "Мультимедиа-файллагъа къарауну джангы адыр бла игилендиреди. Суратны эскизи кёрюннген бетде эскизлени уллу суратлача кёргюзеди. Суратла андан да ариу толуэкранлы интерфейсде кёрюнедиле эмда оригинал разрешениесинде ачылыргъа боллукъдула.",
+ "multimediaviewer-optin-pref": "<span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About Медиа-къараучуну]</span> джандыр",
+ "multimediaviewer-optout-mmv": "Медиа-къараучуну джукълатыу",
+ "multimediaviewer-optin-mmv": "Медиа-къараучуну джандыр",
+ "multimediaviewer-optout-pending-mmv": "Медиа-къараучуну джукълатыу",
+ "multimediaviewer-optin-pending-mmv": "Медиа-къараучуну джандырыу",
+ "multimediaviewer-enable-dialog-header": "Медиа-къараучу джандырылсынмы?",
+ "multimediaviewer-enable-submit-button": "Медиа-къараучуну джандыр"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/ksh.json b/www/wiki/extensions/MultimediaViewer/i18n/ksh.json
new file mode 100644
index 00000000..169a25f1
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/ksh.json
@@ -0,0 +1,63 @@
+{
+ "@metadata": {
+ "authors": [
+ "Purodha"
+ ]
+ },
+ "multimediaviewer-repository-local": "Mih Einzelheijte",
+ "multimediaviewer-datetime-created": "Aanjelaat: $1",
+ "multimediaviewer-datetime-uploaded": "Huhjelahde: $1",
+ "multimediaviewer-credit-fallback": "Dahte övver dä Schrihver belohre",
+ "multimediaviewer-multiple-authors": "{{PLURAL:$1|norr_enne|noch $1 mih Schrihver|un söns keine}} Schrihver",
+ "multimediaviewer-multiple-authors-combine": "$1 un $2",
+ "multimediaviewer-metadata-error": "Mer kunnte kein Eijnzeljeijte övver dat bed lahde, der Jrond: $1",
+ "multimediaviewer-thumbnail-error-retry": "norr_ens versöhke",
+ "multimediaviewer-thumbnail-error-report": "donn dat mällde",
+ "multimediaviewer-license-cc-pd": "Allmende (jemeinfrei, <i lang=\"en\">public domain</i>)",
+ "multimediaviewer-license-pd": "Allmende (jemeinfrei, <i lang=\"en\">public domain</i>)",
+ "multimediaviewer-license-default": "De Lezänz beraache",
+ "multimediaviewer-permission-title": "Einzelheijte övver et Jebruche",
+ "multimediaviewer-permission-viewmore": "Mih aanzeije",
+ "multimediaviewer-about-mmv": "Övver",
+ "multimediaviewer-discuss-mmv": "Klaaf",
+ "multimediaviewer-help-mmv": "Hölp",
+ "multimediaviewer-geolocation": "Posizjuhn: $1",
+ "multimediaviewer-reuse-link": "Donn heh di dattei wigger jävve udder woh ennboue",
+ "multimediaviewer-reuse-loading-placeholder": "Ben am Lahde&nbsp;…",
+ "multimediaviewer-share-tab": "Wigger jävve",
+ "multimediaviewer-embed-tab": "Ennföhje",
+ "multimediaviewer-download-link": "Heh di Dattei eronger lahde",
+ "multimediaviewer-download-preview-link-title": "Em Brauser opmaache",
+ "multimediaviewer-download-original-button-name": "Donn de Ojinahl_Dattei eronger lahde",
+ "multimediaviewer-download-small-button-name": "Donn en verkleijnerte Väsjohn vun dä Dattei eronger lahde",
+ "multimediaviewer-download-medium-button-name": "Donn en meddeljruhße Väsjohn vun dä Dattei eronger lahde",
+ "multimediaviewer-download-large-button-name": "Donn en jruhße Väsjohn vun dä Dattei eronger lahde",
+ "multimediaviewer-link-to-file": "Lengk op di Ojjinahl_Dattei",
+ "multimediaviewer-embed-wt": "Wikkitäx",
+ "multimediaviewer-embed-html": "\n<i lang=\"en\" xml:lang=\"en\" dir=\"ltr\" title=\"HyperText Markup Language\">HTML</i>",
+ "multimediaviewer-embed-explanation": "Nemm heh dä Kohd, öm di Dattei ennzebenge",
+ "multimediaviewer-embed-byline": "Vum $1",
+ "multimediaviewer-embed-license": "Veröffentlesch onger dä Lezänz $1.",
+ "multimediaviewer-embed-via": "Övver $1",
+ "multimediaviewer-default-embed-dimensions": "Schtandatt_Jrühße vum Minnibelldsche",
+ "multimediaviewer-original-embed-dimensions": "Ojinal_Dattei $1",
+ "multimediaviewer-large-embed-dimensions": "Jruhß $1",
+ "multimediaviewer-medium-embed-dimensions": "Meddel $1",
+ "multimediaviewer-small-embed-dimensions": "Klein $1",
+ "multimediaviewer-description-page-button-text": "Mih Einzelheijte övver heh di Dattei",
+ "multimediaviewer-description-page-popup-text": "Mih Einzelheijte övver heh di Dattei op $1",
+ "multimediaviewer-commons-subtitle": "Dat freije Repossetohrejom för Mehdije",
+ "multimediaviewer-view-config": "Enschtällonge",
+ "multimediaviewer-close-popup-text": "Maach heh dat Wärkzüsch zuoh (Esc)",
+ "multimediaviewer-fullscreen-popup-text": "Om jannze Beldscherrem zeije",
+ "multimediaviewer-defullscreen-popup-text": "Nimmih om jannze Beldscherrem zeije",
+ "multimediaviewer-title-popup-text": "Aanjahbe övver di Dattei",
+ "multimediaviewer-credit-popup-text": "Övver de Hääkonnef vun dä dattei",
+ "multimediaviewer-title-popup-text-more": "Ale Aanjahbe belohre",
+ "multimediaviewer-credit-popup-text-more": "Alles övver der Könsler un de Quäll",
+ "multimediaviewer-download-attribution-cta-header": "Do moß der Maacher nänne",
+ "multimediaviewer-download-attribution-cta": "Zeijsch mer, wi",
+ "multimediaviewer-attr-plain": "Pladd un eijfach",
+ "multimediaviewer-options-learn-more": "Mih lässe",
+ "multimediaviewer-option-cancel-button": "Ophühre"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/ku-latn.json b/www/wiki/extensions/MultimediaViewer/i18n/ku-latn.json
new file mode 100644
index 00000000..43a1409c
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/ku-latn.json
@@ -0,0 +1,10 @@
+{
+ "@metadata": {
+ "authors": [
+ "Bikarhêner",
+ "Ghybu"
+ ]
+ },
+ "multimediaviewer-about-mmv": "Derbar",
+ "multimediaviewer-option-cancel-button": "Betal bike"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/lb.json b/www/wiki/extensions/MultimediaViewer/i18n/lb.json
new file mode 100644
index 00000000..9f9d185e
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/lb.json
@@ -0,0 +1,91 @@
+{
+ "@metadata": {
+ "authors": [
+ "Robby"
+ ]
+ },
+ "multimediaviewer-pref": "Media Viewer",
+ "multimediaviewer-optin-pref": "De <span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About Media Viewer]</span> aktivéieren",
+ "multimediaviewer-repository-local": "Méi Detailer",
+ "multimediaviewer-datetime-uploaded": "Eropgelueden: $1",
+ "multimediaviewer-credit-fallback": "Informatioune vum Auteur weisen",
+ "multimediaviewer-multiple-authors": "{{PLURAL:$1|ee weideren Auteur|$1 weider Auteuren}}",
+ "multimediaviewer-multiple-authors-combine": "$1 a(n) $2",
+ "multimediaviewer-metadata-error": "Detailer vum Bild konten net geluede ginn (Feeler: $1)",
+ "multimediaviewer-thumbnail-error-retry": "Nach eng Kéier probéieren",
+ "multimediaviewer-thumbnail-error-report": "de Problem mellen",
+ "multimediaviewer-license-cc-by-4.0": "CC BY 4.0",
+ "multimediaviewer-license-cc-by-sa-4.0": "CC BY-SA 4.0",
+ "multimediaviewer-license-pd": "Ëffentlechen Domaine",
+ "multimediaviewer-license-default": "Lizenz weisen",
+ "multimediaviewer-permission-title": "Detailer vun der Erlaabnes",
+ "multimediaviewer-permission-link-hide": "Konditioune verstoppen",
+ "multimediaviewer-permission-viewmore": "Méi weisen",
+ "multimediaviewer-restriction-communist": "Op dësem Bild si kommunistesch Symboler déi verschiddene Länner kéinte verbuede sinn.",
+ "multimediaviewer-restriction-currency": "Dës Bild ass eng Duerstellung vun enger Währungseenheet an ënnerläit méiglecherweis legale Limitatiounen.",
+ "multimediaviewer-restriction-ihl": "Op dësem Bild si Symboler déi duerch dat internationaalt humanitärt Recht geschützt sinn.",
+ "multimediaviewer-restriction-insignia": "Op dësem Bild sinn offiziell Logoen déi eventuell den Objet vu rechtleche Limitatiounen si kéinten.",
+ "multimediaviewer-restriction-nazi": "Op dësem Bild si Nazi- oder aner faschistesch Symboler déi an eenzelne Länner kéinte verbuede sinn.",
+ "multimediaviewer-about-mmv": "Iwwer",
+ "multimediaviewer-discuss-mmv": "Diskussioun",
+ "multimediaviewer-help-mmv": "Hëllef",
+ "multimediaviewer-optout-mmv": "Media Viewer ausschalten",
+ "multimediaviewer-optin-mmv": "Media Viewer aktivéieren",
+ "multimediaviewer-optout-pending-mmv": "Media Viewer desaktivéieren",
+ "multimediaviewer-optin-pending-mmv": "Media Viewer aktivéieren",
+ "multimediaviewer-optin-help": "Media Viewer gëtt benotzt fir Biller ze weisen.",
+ "multimediaviewer-geolocation": "Plaz: $1",
+ "multimediaviewer-reuse-link": "Dëse Fichier deelen oder abannen",
+ "multimediaviewer-reuse-loading-placeholder": "Lueden...",
+ "multimediaviewer-share-tab": "Deelen",
+ "multimediaviewer-embed-tab": "Abannen",
+ "multimediaviewer-download-link": "Dëse Fichier eroflueden",
+ "multimediaviewer-download-preview-link-title": "Am Browser weisen",
+ "multimediaviewer-download-original-button-name": "Originalfichier eroflueden",
+ "multimediaviewer-download-small-button-name": "Kleng Gréisst eroflueden",
+ "multimediaviewer-download-medium-button-name": "Mëttel Gréisst eroflueden",
+ "multimediaviewer-download-large-button-name": "Grouss Gréisst eroflueden",
+ "multimediaviewer-link-to-page": "Link op d'Beschreiwungssäit vum Fichier",
+ "multimediaviewer-link-to-file": "Link op den Originalfichier",
+ "multimediaviewer-embed-wt": "Wikitext",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-text-embed-credit-text-bl": "Vum $1, $2, $3",
+ "multimediaviewer-text-embed-credit-text-b": "Vum $1, $2",
+ "multimediaviewer-html-embed-credit-text-bl": "Vum $1, $2, $3",
+ "multimediaviewer-html-embed-credit-text-b": "Vum $1, $2",
+ "multimediaviewer-html-embed-credit-link-text": "Link",
+ "multimediaviewer-embed-byline": "Vum $1",
+ "multimediaviewer-embed-license": "Lizenzéiert ënner $1.",
+ "multimediaviewer-embed-via": "Iwwer $1.",
+ "multimediaviewer-original-embed-dimensions": "Originalfichier $1",
+ "multimediaviewer-large-embed-dimensions": "Grouss $1",
+ "multimediaviewer-medium-embed-dimensions": "Mëttel $1",
+ "multimediaviewer-small-embed-dimensions": "Kleng $1",
+ "multimediaviewer-description-page-button-text": "Méi Detailer iwwer dëse Fichier",
+ "multimediaviewer-description-page-popup-text": "Méi Detailer iwwer dëse Fichier op $1",
+ "multimediaviewer-view-expanded": "Mam Media Viewer opmaachen",
+ "multimediaviewer-view-config": "Astellung",
+ "multimediaviewer-close-popup-text": "Dësen Tool zoumaachen (Esc)",
+ "multimediaviewer-defullscreen-popup-text": "Aus dem ganzen Ecran erausgoen",
+ "multimediaviewer-next-image-alt-text": "Nächst Bild weisen",
+ "multimediaviewer-prev-image-alt-text": "Viregt Bild weisen",
+ "multimediaviewer-title-popup-text": "Beschreiwung",
+ "multimediaviewer-credit-popup-text": "Informatioun iwwer den Auteur an d'Quell",
+ "multimediaviewer-title-popup-text-more": "Kompletten Beschreiwung weisen",
+ "multimediaviewer-credit-popup-text-more": "Weist all Informatiounen iwwer den Auteur an d'Quell",
+ "multimediaviewer-download-attribution-cta-header": "Dir musst eng Referenz op den Auteur maachen",
+ "multimediaviewer-download-attribution-cta": "Weist mir wéi",
+ "multimediaviewer-reuse-warning-deletion": "Et gouf virgeschloen dëse Fichier ze läschen.",
+ "multimediaviewer-reuse-warning-nonfree": "Dëse Fichier huet keng fräi Lizenz.",
+ "multimediaviewer-reuse-warning-noattribution": "Dëse Fichier huet keng Informatiounen iwwer den Auteur.",
+ "multimediaviewer-options-dialog-header": "Media Viewer ausschalten?",
+ "multimediaviewer-options-learn-more": "Fir méi ze wëssen",
+ "multimediaviewer-option-submit-button": "Media Viewer ausschalten",
+ "multimediaviewer-option-cancel-button": "Ofbriechen",
+ "multimediaviewer-disable-confirmation-header": "Dir hutt de Media Viewer ausgeschalt",
+ "multimediaviewer-enable-dialog-header": "Media Viewer aschalten?",
+ "multimediaviewer-enable-submit-button": "Media Viewer aktivéieren",
+ "multimediaviewer-enable-confirmation-header": "Dir hutt de Media Viewer fir all Fichieren ageschalt.",
+ "multimediaviewer-enable-alert": "De Media Viewer ass elo ausgeschalt",
+ "multimediaviewer-disable-info-title": "Dir hutt de MediaViewer ausgeschalt"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/lki.json b/www/wiki/extensions/MultimediaViewer/i18n/lki.json
new file mode 100644
index 00000000..2967e887
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/lki.json
@@ -0,0 +1,10 @@
+{
+ "@metadata": {
+ "authors": [
+ "Lakzon"
+ ]
+ },
+ "multimediaviewer-link-to-page": "پیوند به صفحهٔ توضیحات پرونده",
+ "multimediaviewer-title-popup-text": "توضیحۀل",
+ "multimediaviewer-title-popup-text-more": "مشاهدهٔ کامل توضیحات"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/lrc.json b/www/wiki/extensions/MultimediaViewer/i18n/lrc.json
new file mode 100644
index 00000000..c827bf74
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/lrc.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Mogoeilor"
+ ]
+ },
+ "multimediaviewer-repository-local": "بيشتر يا بيئريت"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/lt.json b/www/wiki/extensions/MultimediaViewer/i18n/lt.json
new file mode 100644
index 00000000..94638bc1
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/lt.json
@@ -0,0 +1,101 @@
+{
+ "@metadata": {
+ "authors": [
+ "Mantak111",
+ "Robotukas11",
+ "Hugo.arg",
+ "Gediminas",
+ "Eitvys200",
+ "Zygimantus",
+ "Manvydasz"
+ ]
+ },
+ "multimediaviewer-desc": "Išplėsti labiau miniatiūras esant pilno dydžio vartotojo aplinkai.",
+ "multimediaviewer-pref": "Nuotraukų peržvalgos įrankis",
+ "multimediaviewer-optin-pref": "Įjungti <span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About nuotraukų peržvalgos programą]</span>",
+ "multimediaviewer-file-page": "Eiti į atitinkamą rinkmenos puslapį",
+ "multimediaviewer-repository-local": "Plačiau",
+ "multimediaviewer-datetime-created": "Sukurta: $1",
+ "multimediaviewer-datetime-uploaded": "Įkelta: $1",
+ "multimediaviewer-credit-fallback": "Žiūrėti informaciją apie autorių",
+ "multimediaviewer-multiple-authors": "{{PLURAL:$1|dar vienas autorius|dar $1 autoriai|dar $1 autorių}}",
+ "multimediaviewer-metadata-error": "Nepavyksta įkelti vaizdo duomenų (klaida: $1)",
+ "multimediaviewer-thumbnail-error": "Klaida: nepavyksta pakrauti miniatiūros duomenų. $1",
+ "multimediaviewer-thumbnail-error-retry": "bandyti dar kartą",
+ "multimediaviewer-license-cc-pd": "Viešo naudojimo",
+ "multimediaviewer-license-pd": "Viešo naudojimo",
+ "multimediaviewer-license-default": "Žiūrėti licenciją",
+ "multimediaviewer-permission-title": "Leidimo informacija",
+ "multimediaviewer-permission-link": "žiūrėti sąlygas",
+ "multimediaviewer-permission-viewmore": "Rodyti daugiau",
+ "multimediaviewer-about-mmv": "Apie",
+ "multimediaviewer-discuss-mmv": "Aptarimas",
+ "multimediaviewer-help-mmv": "Pagalba",
+ "multimediaviewer-optout-mmv": "Išjungti nuotraukų peržiūros įrankį",
+ "multimediaviewer-optin-mmv": "Įjungti nuotraukų peržvalgos programą",
+ "multimediaviewer-optout-pending-mmv": "Nuotraukų peržiūros įrankis išjungiamas",
+ "multimediaviewer-optin-pending-mmv": "Nuotraukų peržiūros įrankis įjungiamas",
+ "multimediaviewer-optout-help": "Nuotraukų peržiūros įrankis daugiau nebenaudojamas paveikslėlių rodymui. Kad vėl būtų naudojimas, paspauskite \"{{int:multimediaviewer-view-expanded}}\" mygtuką, esantį greta bet kurio paveikslėlio. Tada spauskite \"{{int:multimediaviewer-optin-mmv}}\".",
+ "multimediaviewer-optin-help": "Nuotraukų peržiūros įrankis bus naudojamas paveikslėlių atvaizdavimui.",
+ "multimediaviewer-geolocation": "Padėtis: $1",
+ "multimediaviewer-reuse-link": "Dalintis arba įterpti šią nuotrauką",
+ "multimediaviewer-reuse-loading-placeholder": "Kraunama...",
+ "multimediaviewer-share-tab": "Dalintis",
+ "multimediaviewer-embed-tab": "Įterpti",
+ "multimediaviewer-download-link": "Atsisiųsti šią nuotrauką",
+ "multimediaviewer-download-preview-link-title": "Peržiūra naršyklėje",
+ "multimediaviewer-download-original-button-name": "Atsisiųsti pradinę rinkmeną",
+ "multimediaviewer-download-small-button-name": "Atsisiųsti mažu dydžiu",
+ "multimediaviewer-download-medium-button-name": "Atsisiųsti vidutiniu dydžiu",
+ "multimediaviewer-download-large-button-name": "Atsisiųsti dideliu dydžiu",
+ "multimediaviewer-link-to-page": "Nuoroda į aprašymo puslapį",
+ "multimediaviewer-link-to-file": "Nuoroda į pirminę rinkmeną",
+ "multimediaviewer-share-explanation": "Kopijuoti ir laisvai dalintis šia nuoroda",
+ "multimediaviewer-embed-wt": "Wiki-tekstas",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-embed-explanation": "Naudokite šį kodą rinkmenos įterpimui",
+ "multimediaviewer-html-embed-credit-link-text": "Nuoroda",
+ "multimediaviewer-embed-byline": "Naudotojo $1 kūrinys",
+ "multimediaviewer-embed-license": "Licencijuotas kaip $1.",
+ "multimediaviewer-embed-via": "Iš tinklavietės $1.",
+ "multimediaviewer-default-embed-dimensions": "Miniatiūros dydis pagal nutylėjimą",
+ "multimediaviewer-original-embed-dimensions": "Pradinio dydžio rinkmena $1",
+ "multimediaviewer-large-embed-dimensions": "Didelis $1",
+ "multimediaviewer-medium-embed-dimensions": "Vidutinis $1",
+ "multimediaviewer-small-embed-dimensions": "Mažas $1",
+ "multimediaviewer-description-page-button-text": "Daugiau duomenų apie šią rinkmeną",
+ "multimediaviewer-description-page-popup-text": "Daugiau duomenų apie šią rinkmeną tinklalapyje $1",
+ "multimediaviewer-commons-subtitle": "Laisvųjų rinkmenų saugykla",
+ "multimediaviewer-view-expanded": "Atverti su nuotraukų peržvalgos įrankiu",
+ "multimediaviewer-view-config": "Sąranga",
+ "multimediaviewer-close-popup-text": "Užverti šį įrankį (Esc)",
+ "multimediaviewer-fullscreen-popup-text": "Rodyti per visą ekraną",
+ "multimediaviewer-defullscreen-popup-text": "Panaikinti būseną „per visą ekraną“",
+ "multimediaviewer-next-image-alt-text": "Rodyti kitą vaizdą",
+ "multimediaviewer-prev-image-alt-text": "Rodyti ankstesnį vaizdą",
+ "multimediaviewer-title-popup-text": "Aprašymas",
+ "multimediaviewer-credit-popup-text": "Žinios apie kūrėją ir šaltinį",
+ "multimediaviewer-title-popup-text-more": "Žiūrėti visą aprašymą",
+ "multimediaviewer-credit-popup-text-more": "Žiūrėti visas žinias apie kūrėją ir šaltinį",
+ "multimediaviewer-download-attribution-cta-header": "Jums būtina nurodyti autorių",
+ "multimediaviewer-download-optional-attribution-cta-header": "Jūs galite nurodyti autorių",
+ "multimediaviewer-download-attribution-cta": "Parodyti man, kaip",
+ "multimediaviewer-attr-plain": "Paprastas",
+ "multimediaviewer-options-tooltip": "Įjungti arba išjungti nuotraukų peržvalgos įrankį",
+ "multimediaviewer-options-dialog-header": "Išjungti nuotraukų peržvalgos įrankį?",
+ "multimediaviewer-options-text-header": "Panaikinti šį peržiūros įrankį visoms rinkmenoms.",
+ "multimediaviewer-options-text-body": "Jūs galite jį įjungti vėliau rinkmenų aprašymo puslapyje.",
+ "multimediaviewer-options-learn-more": "Sužinokite daugiau",
+ "multimediaviewer-option-submit-button": "Išjungti nuotraukų peržiūros įrankį",
+ "multimediaviewer-option-cancel-button": "Atšaukti",
+ "multimediaviewer-disable-confirmation-header": "Jūs išjungėte nuotraukų peržiūros įrankį",
+ "multimediaviewer-disable-confirmation-text": "Kitąsyk kai paspausite ant miniatiūros $1 svetainėje, jus nukreips tiesiai į vaizdo aprašymo puslapį.",
+ "multimediaviewer-enable-dialog-header": "Įjungti nuotraukų peržvalgos įrankį?",
+ "multimediaviewer-enable-text-header": "Įjungti šį nuotraukų peržiūros įrankį pagal nutylėjimą visoms rinkmenoms.",
+ "multimediaviewer-enable-submit-button": "Įjungti nuotraukų peržiūros įrankį",
+ "multimediaviewer-enable-confirmation-header": "Išjungėte nuotraukų peržvalgos įrankį visų atmainų rinkmenoms",
+ "multimediaviewer-enable-confirmation-text": "Kitąsyk, kai paspausite ant miniatiūros $1 svetainėje, bus naudojamas nuotraukų peržvalgos įrankis.",
+ "multimediaviewer-enable-alert": "Nuotraukų peržvalgos įrankis dabar išjungtas",
+ "multimediaviewer-disable-info-title": "Jūs išjungėte nuotraukų peržiūros įrankį",
+ "multimediaviewer-disable-info": "Jūs vis dar galite peržiūrėti atskirus vaizdus su nuotraukų peržvalgos įrankiu"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/lv.json b/www/wiki/extensions/MultimediaViewer/i18n/lv.json
new file mode 100644
index 00000000..6f7ac6c5
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/lv.json
@@ -0,0 +1,69 @@
+{
+ "@metadata": {
+ "authors": [
+ "Papuass",
+ "Edgars2007"
+ ]
+ },
+ "multimediaviewer-pref": "Multimediju skatīklis",
+ "multimediaviewer-file-page": "Doties uz atbilstošo faila lapu",
+ "multimediaviewer-repository-local": "Vairāk informācijas",
+ "multimediaviewer-datetime-created": "Izveidots: $1",
+ "multimediaviewer-datetime-uploaded": "Augšupielādēts: $1",
+ "multimediaviewer-multiple-authors-combine": "$1 un $2",
+ "multimediaviewer-metadata-error": "Kļūda: Nevarēja ielādēt attēla datus. $1",
+ "multimediaviewer-thumbnail-error": "Kļūda: Nevarēja ielādēt sīktēlu datus. $1",
+ "multimediaviewer-thumbnail-error-report": "ziņot par problēmu",
+ "multimediaviewer-license-cc-pd": "Neaizsargāts darbs",
+ "multimediaviewer-license-pd": "Neaizsargāts darbs",
+ "multimediaviewer-license-default": "Skatīt licenci",
+ "multimediaviewer-permission-title": "Atļauju detaļas",
+ "multimediaviewer-permission-link": "skatīt noteikumus",
+ "multimediaviewer-permission-viewmore": "Skatīt vairāk",
+ "multimediaviewer-about-mmv": "Par",
+ "multimediaviewer-discuss-mmv": "Diskusija",
+ "multimediaviewer-help-mmv": "Palīdzība",
+ "multimediaviewer-optout-mmv": "Deaktivizēt Media Viewer",
+ "multimediaviewer-optin-mmv": "Aktivizēt multimediju skatīkli",
+ "multimediaviewer-optout-pending-mmv": "Atspējo multimediju skatīkli",
+ "multimediaviewer-optin-pending-mmv": "Iepsējo multimediju skatīkli",
+ "multimediaviewer-optout-help": "Multimediju skatīklis vairs netiks izmantots attēlu apskatei. Lai atkal to izmantotu, klikšķiniet uz \"Izplest\" pogas blakus jebkuram attēlam. tad klikšķiniet uz \"{{int:multimediaviewer-optin-mmv}}\".",
+ "multimediaviewer-optin-help": "Multimediju skatīklis tiks izmantots attēlu apskatei.",
+ "multimediaviewer-geolocation": "Atrašanās vieta: $1",
+ "multimediaviewer-reuse-link": "Dalīties vai iegult šo failu",
+ "multimediaviewer-reuse-loading-placeholder": "Ielādē…",
+ "multimediaviewer-share-tab": "Dalīties",
+ "multimediaviewer-embed-tab": "Ievietot",
+ "multimediaviewer-download-link": "Lejupielādēt šo failu",
+ "multimediaviewer-download-preview-link-title": "Skatīt pārlūkprogrammā",
+ "multimediaviewer-download-original-button-name": "Lejupielādēt oriģinālo failu",
+ "multimediaviewer-download-small-button-name": "Lejupielādēt maza izmēra failu",
+ "multimediaviewer-download-medium-button-name": "Lejupielādēt vidēja izmēra failu",
+ "multimediaviewer-download-large-button-name": "Lejupielādēt liela izmēra failu",
+ "multimediaviewer-link-to-page": "Saite uz attēla aprakstu lapu",
+ "multimediaviewer-link-to-file": "Saite uz oriģinālo attēlu",
+ "multimediaviewer-share-explanation": "Nokopējiet un brīvi dalieties ar saiti",
+ "multimediaviewer-embed-wt": "Vikiteksts",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-embed-explanation": "Izmanto šo kodu, lai iegultu failu",
+ "multimediaviewer-html-embed-credit-link-text": "Saite",
+ "multimediaviewer-embed-license": "Licencēts saskaņā ar $1.",
+ "multimediaviewer-default-embed-dimensions": "Noklusējuma sīktēlu izmērs",
+ "multimediaviewer-original-embed-dimensions": "Sākotnējais fails $1",
+ "multimediaviewer-large-embed-dimensions": "Liels $1",
+ "multimediaviewer-medium-embed-dimensions": "Vidējs $1",
+ "multimediaviewer-small-embed-dimensions": "Mazs $1",
+ "multimediaviewer-description-page-button-text": "Vairāk datu par šo failu",
+ "multimediaviewer-commons-subtitle": "Brīvā multimediju krātuve",
+ "multimediaviewer-view-expanded": "Atvērt Media Viewer",
+ "multimediaviewer-view-config": "Konfigurācija",
+ "multimediaviewer-close-popup-text": "Aizvērt šo rīku (Esc)",
+ "multimediaviewer-fullscreen-popup-text": "Parādīt pilnekrāna režīmā",
+ "multimediaviewer-defullscreen-popup-text": "Iziet no pilnekrāna režīma",
+ "multimediaviewer-next-image-alt-text": "Rādīt nākamo attēlu",
+ "multimediaviewer-prev-image-alt-text": "Rādīt iepriekšējo attēlu",
+ "multimediaviewer-title-popup-text": "Apraksts",
+ "multimediaviewer-reuse-warning-nonfree": "Šim failam nav brīvas licences",
+ "multimediaviewer-options-learn-more": "Uzzināt vairāk",
+ "multimediaviewer-option-cancel-button": "Atcelt"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/lzh.json b/www/wiki/extensions/MultimediaViewer/i18n/lzh.json
new file mode 100644
index 00000000..2c4bdc37
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/lzh.json
@@ -0,0 +1,14 @@
+{
+ "@metadata": {
+ "authors": [
+ "StephDC",
+ "Jason924tw"
+ ]
+ },
+ "multimediaviewer-repository-local": "顯細",
+ "multimediaviewer-help-mmv": "助",
+ "multimediaviewer-reuse-loading-placeholder": "載之…",
+ "multimediaviewer-download-link": "取檔",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-attr-plain": "純字"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/mg.json b/www/wiki/extensions/MultimediaViewer/i18n/mg.json
new file mode 100644
index 00000000..1e305fdb
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/mg.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Jagwar"
+ ]
+ },
+ "multimediaviewer-permission-viewmore": "Hijery be kokoa"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/mk.json b/www/wiki/extensions/MultimediaViewer/i18n/mk.json
new file mode 100644
index 00000000..21ea6f9f
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/mk.json
@@ -0,0 +1,137 @@
+{
+ "@metadata": {
+ "authors": [
+ "Bjankuloski06"
+ ]
+ },
+ "multimediaviewer-desc": "Зголемување на минијатурите на цел екран.",
+ "multimediaviewer-pref": "Прегледувач",
+ "multimediaviewer-pref-desc": "Дава поубаво прегледување на слики на страници. Ги прикажува поголеми на страниците со минијатури. Можат да се прегледуваат и на цел екран.",
+ "multimediaviewer-optin-pref": "Вклучи го <span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About Мултимедијалниот изведувач]</span>",
+ "multimediaviewer-file-page": "Оди на соодветната податотечна страница",
+ "multimediaviewer-repository-local": "Поподробно",
+ "multimediaviewer-datetime-created": "Создадено: $1",
+ "multimediaviewer-datetime-uploaded": "Подигнато: $1",
+ "multimediaviewer-credit": "$1 — $2",
+ "multimediaviewer-credit-fallback": "Погледнете авторски информации",
+ "multimediaviewer-multiple-authors": "{{PLURAL:$1|уште еден автор|уште $1 автори}}",
+ "multimediaviewer-multiple-authors-combine": "$1 и $2",
+ "multimediaviewer-metadata-error": "Не можев да ги вчитам податоците за сликата. (грешка: $1)",
+ "multimediaviewer-thumbnail-error": "За жал, не можев да ја прикажам податотеката",
+ "multimediaviewer-thumbnail-error-description": "Имаме технички проблем. Можете да $1 или $3 ако упрно се јавува. Грешка: $2",
+ "multimediaviewer-thumbnail-error-retry": "пробај пак",
+ "multimediaviewer-thumbnail-error-report": "го пријавите проблемот",
+ "multimediaviewer-license-cc-by-4.0": "CC BY 4.0",
+ "multimediaviewer-license-cc-by-sa-4.0": "CC BY-SA 4.0",
+ "multimediaviewer-license-cc-pd": "Јавна сопственост",
+ "multimediaviewer-license-pd": "Јавна сопственост",
+ "multimediaviewer-license-default": "Погл. лиценцата",
+ "multimediaviewer-permission-title": "Подробности за дозволата",
+ "multimediaviewer-permission-link": "погл. услови",
+ "multimediaviewer-permission-link-hide": "скриј услови",
+ "multimediaviewer-permission-viewmore": "Погл. уште",
+ "multimediaviewer-restriction-2257": "Оваа слика содржи јасно сексуална содржина, која може да подлежи на Законот за заштита на детето и контрола врз непристојниот материјал на САД (''Child Protection and Obscenity Enforcement Act'').",
+ "multimediaviewer-restriction-aus-reserve": "Сликава е фотографирана во резерват на Државната заедница Австралија и не може да се користи за стекнување на финансиска добивка без да се добие дозвола за тоа.",
+ "multimediaviewer-restriction-communist": "Оваа слика содржи комунистички обележја кои се забранети во некои земји.",
+ "multimediaviewer-restriction-costume": "На сликава е прикажано костимирање кое може да подлежи на правни ограничувања.",
+ "multimediaviewer-restriction-currency": "На сликава е претставена платежна единица која може да подлежи на правни ограничувања.",
+ "multimediaviewer-restriction-design": "Дизајнот на сликава може да е заштитен со авторски права, што би значело дека подложи на правни ограничувања.",
+ "multimediaviewer-restriction-fan-art": "Оваа слика е обожавателско уметничко дело, и нејзината пренамена може да подлежи на правни ограничувања.",
+ "multimediaviewer-restriction-ihl": "Оваа слика содржи симболи ограничени од меѓународното хуманитарно право.",
+ "multimediaviewer-restriction-insignia": "Сликава содржи службени обележја што може да подлежат на правни ограничувања.",
+ "multimediaviewer-restriction-ita-mibac": "Оваа слика прикажува културно наследство на Италија и подлежи на ограничувањата пропишани од италијанското право.",
+ "multimediaviewer-restriction-nazi": "Оваа слика содржи нацистички или други фашистички обележја кои се забранети во некои земји.",
+ "multimediaviewer-restriction-personality": "На сликава се прикажани личности кои може да имаат права што ограничуваат извесни нејзини употреби без претходно добиена согласност.",
+ "multimediaviewer-restriction-trademarked": "Сликава има содржини што може да подлежат на законите за заштитен знак.",
+ "multimediaviewer-restriction-default": "Сликава може да е предмет на ограничувања согласно правни одредби вон авторското право. Погледајте ја описната страница на податотеката за повеќе информации.",
+ "multimediaviewer-restriction-default-and-others": "Сликава може да подлежи на понатамошни ограничувања согласно други правни одредби вон авторското право. Погледајте ја описната страница на податотеката за повеќе информации.",
+ "multimediaviewer-about-mmv": "За прегледувачот",
+ "multimediaviewer-discuss-mmv": "Разговор",
+ "multimediaviewer-help-mmv": "Ппомош",
+ "multimediaviewer-optout-mmv": "Исклучи го Прегледувачот",
+ "multimediaviewer-optin-mmv": "Вклучи го Прегледувачот",
+ "multimediaviewer-optout-pending-mmv": "Исклучување на Прегледувачот",
+ "multimediaviewer-optin-pending-mmv": "Вклучување на Прегледувачот",
+ "multimediaviewer-optout-help": "Прегледувачот повеќе нема да се користи за прикажување на слики. Ако сакате да го користите повторно, стиснете на копчето „{{int:multimediaviewer-view-expanded}}“ веднаш до секоја слика. Потоа стниснете на „{{int:multimediaviewer-optin-mmv}}“.",
+ "multimediaviewer-optin-help": "Прегледувачот ќе се користи за прикажување на слики.",
+ "multimediaviewer-geoloc-north": "СГШ",
+ "multimediaviewer-geoloc-east": "ИГД",
+ "multimediaviewer-geoloc-south": "ЈГШ",
+ "multimediaviewer-geoloc-west": "ЗГД",
+ "multimediaviewer-geoloc-coords": "$1, $2",
+ "multimediaviewer-geolocation": "Местоположба: $1",
+ "multimediaviewer-reuse-link": "Сподели/Вметни ја податотекава",
+ "multimediaviewer-reuse-loading-placeholder": "Вчитувам…",
+ "multimediaviewer-reuse-copy-share": "Изберете и прекопирајте ја врската (ако е поддржано) за споделување на податотекава",
+ "multimediaviewer-reuse-copy-embed": "Изберете и прекопирајте го кодот (ако е поддржано) за вметнување на податотекава",
+ "multimediaviewer-share-tab": "Сподели",
+ "multimediaviewer-embed-tab": "Вметни",
+ "multimediaviewer-download-link": "Преземи ја податотекава",
+ "multimediaviewer-download-preview-link-title": "Погледај во прелистувач",
+ "multimediaviewer-download-original-button-name": "Преземи ја изворната податотека",
+ "multimediaviewer-download-small-button-name": "Преземи мало",
+ "multimediaviewer-download-medium-button-name": "Преземи средно",
+ "multimediaviewer-download-large-button-name": "Преземи големо",
+ "multimediaviewer-link-to-page": "Врска до описната страница на податотеката",
+ "multimediaviewer-link-to-file": "Врска до изворната податотека",
+ "multimediaviewer-share-explanation": "Ископирајте ја врската и слободно споделувајте ја",
+ "multimediaviewer-embed-wt": "Викитекст",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-embed-explanation": "Со овој код можете некаде да ја вметнете податотеката",
+ "multimediaviewer-text-embed-credit-text-bl": "Од $1, $2, $3",
+ "multimediaviewer-text-embed-credit-text-b": "Од $1, $2",
+ "multimediaviewer-html-embed-credit-text-bl": "Од $1, $2, $3",
+ "multimediaviewer-html-embed-credit-text-b": "Од $1, $2",
+ "multimediaviewer-html-embed-credit-link-text": "Врска",
+ "multimediaviewer-embed-byline": "Од $1",
+ "multimediaviewer-embed-license": "Под лиценцата $1.",
+ "multimediaviewer-embed-via": "Преку $1.",
+ "multimediaviewer-default-embed-dimensions": "Стандардна големина на минијатурата",
+ "multimediaviewer-original-embed-dimensions": "Изворна податотека $1",
+ "multimediaviewer-large-embed-dimensions": "Голема $1",
+ "multimediaviewer-medium-embed-dimensions": "Средна $1",
+ "multimediaviewer-small-embed-dimensions": "Мала $1",
+ "multimediaviewer-embed-dimensions": "$1 × $2 пиксели",
+ "multimediaviewer-embed-dimensions-separated": "— $1",
+ "multimediaviewer-description-page-button-text": "Поподробно за податотекава",
+ "multimediaviewer-description-page-popup-text": "Поподробно за податотекава на $1",
+ "multimediaviewer-commons-subtitle": "Слободно складиште на медиумски содржини",
+ "multimediaviewer-view-expanded": "Отвори во Прегледувачот",
+ "multimediaviewer-view-config": "Нагодување",
+ "multimediaviewer-close-popup-text": "Затворете ја алаткава (Esc)",
+ "multimediaviewer-fullscreen-popup-text": "Прикажи на цел екран",
+ "multimediaviewer-defullscreen-popup-text": "Излези од полн екран",
+ "multimediaviewer-next-image-alt-text": "Следна слика",
+ "multimediaviewer-prev-image-alt-text": "Претходна слика",
+ "multimediaviewer-title-popup-text": "Опис",
+ "multimediaviewer-credit-popup-text": "Информации за авторот и изворот",
+ "multimediaviewer-title-popup-text-more": "Погл. целосниот опис",
+ "multimediaviewer-credit-popup-text-more": "Погл. целосните информации за звтор и извор",
+ "multimediaviewer-download-attribution-cta-header": "Ќе треба да го наведете авторот",
+ "multimediaviewer-download-optional-attribution-cta-header": "Можете да го наведете авторот",
+ "multimediaviewer-download-attribution-cta": "Покажи ми како",
+ "multimediaviewer-download-attribution-copy": "Одберете и копирајте го (ако е поддражно) приписниот текст за податотекава.",
+ "multimediaviewer-reuse-warning-deletion": "Оваа податотека е ставена на разгледување за бришење.",
+ "multimediaviewer-reuse-warning-nonfree": "Оваа податотека нема слободна лиценца.",
+ "multimediaviewer-reuse-warning-noattribution": "Оваа податотека нема приписни податоци.",
+ "multimediaviewer-reuse-warning-generic": "Погледајте ги [$1 поединостите] пред да ја употребите.",
+ "multimediaviewer-attr-plain": "Прост",
+ "multimediaviewer-options-tooltip": "Вклучи или исклучи го Прегледувачот",
+ "multimediaviewer-options-dialog-header": "Да го исклучам Прегледувачот?",
+ "multimediaviewer-options-text-header": "Прескокнувај ја функцијата за преглед кај сите податотеки.",
+ "multimediaviewer-options-text-body": "Можете подоцна да ја вклучите преку делот поподробно за податотеката.",
+ "multimediaviewer-options-learn-more": "Дознајте повеќе",
+ "multimediaviewer-option-submit-button": "Исклучи го Прегледувачот",
+ "multimediaviewer-option-cancel-button": "Откажи",
+ "multimediaviewer-disable-confirmation-header": "Прегледувачот ви е исклучен.",
+ "multimediaviewer-disable-confirmation-text": "Следен пат кога ќе стиснете на минијатура на $1, право ќе ви се прикажат сите подробности за податотеката",
+ "multimediaviewer-enable-dialog-header": "Да го вклучам Прегледувачот?",
+ "multimediaviewer-enable-text-header": "Вклучи ја можноста за преглед на сите податотеки по основно.",
+ "multimediaviewer-enable-submit-button": "Вклучи во Прегледувачот",
+ "multimediaviewer-enable-confirmation-header": "Го вклучивте Прегледувачот за сите податотеки",
+ "multimediaviewer-enable-confirmation-text": "Следен пат кога ќе стиснете на минијатура на $1, податотеката ќе ви се отвори во Прегледувачот.",
+ "multimediaviewer-enable-alert": "Прегледувачот сега ви е исклучен",
+ "multimediaviewer-disable-info-title": "Прегледувачот ви е исклучен",
+ "multimediaviewer-disable-info": "Сè уште можете да прегледувате поединечни податотеки со Прегледувачот.",
+ "multimediaviewer-errorreport-privacywarning": "Во извештајот се приложени подровности за грешката, кои ќе бидат јавно видливи. Доколку тоа ви пречи, можете да го измените извештајот подолу, отстранувајќи ги податоците што не сакате да се знаат."
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/ml.json b/www/wiki/extensions/MultimediaViewer/i18n/ml.json
new file mode 100644
index 00000000..94caa22e
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/ml.json
@@ -0,0 +1,144 @@
+{
+ "@metadata": {
+ "authors": [
+ "Clockery",
+ "Praveenp",
+ "Macofe"
+ ]
+ },
+ "multimediaviewer-desc": "ലഘുചിത്രങ്ങൾ ഫുൾസ്ക്രീനായി വികസിപ്പിക്കുക",
+ "multimediaviewer-pref": "മീഡിയ ദർശനോപാധി",
+ "multimediaviewer-pref-desc": "ഈ പുതിയ ഉപകരണമുപയോഗിച്ച് താങ്കളുടെ മീഡിയ ദർശനാനുഭവം മെച്ചപ്പെടുത്തൂ. ലഘുചിത്രങ്ങൾ ഉപയോഗിച്ചിരിക്കുന്ന താളുകളിലെ ചിത്രങ്ങൾ ഇതുപയോഗിച്ച് വലുതായി കാണാം. ചിത്രങ്ങൾ സുന്ദരമായ ഫുൾസ്ക്രീൻ സമ്പർക്കമുഖ രൂപത്തിലോ, പൂർണ്ണവലിപ്പത്തിലോ കാണാനാവുന്നതാണ്.",
+ "multimediaviewer-optin-pref": "<span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About മീഡിയ കാണൽ സൗകര്യം]</span> സജ്ജമാക്കുക",
+ "multimediaviewer-file-page": "ബന്ധപ്പെട്ട പ്രമാണത്താളിലേയ്ക്ക് പോവുക",
+ "multimediaviewer-repository-local": "വിശദവിവരങ്ങൾ",
+ "multimediaviewer-datetime-created": "സൃഷ്ടിച്ചത്: $1",
+ "multimediaviewer-datetime-uploaded": "അപ്‌ലോഡ് ചെയ്തത്: $1",
+ "multimediaviewer-credit-fallback": "രചയിതാവിന്റെ വിവരങ്ങൾ കാണുക",
+ "multimediaviewer-multiple-authors": "{{PLURAL:$1|ഒരു സ്രഷ്ടാവ് കൂടി|$1 സ്രഷ്ടാക്കൾ കൂടി}}",
+ "multimediaviewer-multiple-authors-combine": "$1 ഒപ്പം $2",
+ "multimediaviewer-metadata-error": "ചിത്രത്തിന്റെ വിവരങ്ങൾ എടുക്കാൻ കഴിഞ്ഞില്ല (പിഴവ്: $1)",
+ "multimediaviewer-thumbnail-error": "ക്ഷമിക്കണം, പ്രമാണം പ്രദർശിപ്പിക്കാനാവില്ല",
+ "multimediaviewer-thumbnail-error-description": "എന്തോ ഒരു സാങ്കേതിക പ്രശ്നമുണ്ടെന്ന് തോന്നുന്നു. താങ്കൾ ഒന്നെങ്കിൽ $1 അല്ലെങ്കിൽ ഇത് സ്ഥിരമായിട്ട് ഉണ്ടെങ്കിൽ $3.\nപിഴവ്: $2",
+ "multimediaviewer-thumbnail-error-retry": "വീണ്ടും ശ്രമിക്കുക",
+ "multimediaviewer-thumbnail-error-report": "പ്രശ്നമാണെന്ന് അറിയിക്കുക",
+ "multimediaviewer-license-cc-by-1.0": "സി.സി. ബൈ 1.0",
+ "multimediaviewer-license-cc-sa-1.0": "സി.സി. എസ്.എ. 1.0",
+ "multimediaviewer-license-cc-by-sa-1.0": "സി.സി. ബൈ-എസ്.എ. 1.0",
+ "multimediaviewer-license-cc-by-2.0": "സി.സി. ബൈ 2.0",
+ "multimediaviewer-license-cc-by-sa-2.0": "സി.സി. ബൈ-എസ്.എ. 2.0",
+ "multimediaviewer-license-cc-by-2.1": "സി.സി. ബൈ 2.1",
+ "multimediaviewer-license-cc-by-sa-2.1": "സി.സി. ബൈ-എസ്.എ. 2.1",
+ "multimediaviewer-license-cc-by-2.5": "സി.സി. ബൈ 2.5",
+ "multimediaviewer-license-cc-by-sa-2.5": "സി.സി. ബൈ-എസ്.എ. 2.5",
+ "multimediaviewer-license-cc-by-3.0": "സി.സി. ബൈ 3.0",
+ "multimediaviewer-license-cc-by-sa-3.0": "സി.സി. ബൈ-എസ്.എ. 3.0",
+ "multimediaviewer-license-cc-by-4.0": "സി.സി. ബൈ 4.0",
+ "multimediaviewer-license-cc-by-sa-4.0": "സി.സി. ബൈ-എസ്.എ. 4.0",
+ "multimediaviewer-license-cc-pd": "പൊതുസഞ്ചയം",
+ "multimediaviewer-license-cc-zero": "സി.സി.0",
+ "multimediaviewer-license-pd": "പൊതുസഞ്ചയം",
+ "multimediaviewer-license-default": "ഉപയോഗാനുമതി കാണുക",
+ "multimediaviewer-permission-title": "അനുമതി വിവരങ്ങൾ",
+ "multimediaviewer-permission-link": "നിബന്ധനകൾ കാണുക",
+ "multimediaviewer-permission-link-hide": "വ്യവസ്ഥകൾ മറയ്ക്കുക",
+ "multimediaviewer-permission-viewmore": "കൂടുതൽ കാണുക",
+ "multimediaviewer-restriction-2257": "അമേരിക്കൻ ഐക്യനാടുകളിലെ ശിശു സംരക്ഷണ, അശ്ലീലതാ തടയൽ നിയമത്തിനു ബാധകമായേക്കാവുന്ന ലൈംഗിക ചിത്രം ഉൾക്കൊള്ളുന്നുണ്ട്.",
+ "multimediaviewer-restriction-aus-reserve": "ഈ ചിത്രം ഓസ്ട്രേലിയൻ കോമൺവെൽത്ത് റിസർവിൽ നിന്നെടുത്തതും വ്യാപാരോദ്ദേശത്തോടെ ഉപയോഗിക്കൻ അനുമതിയെടുക്കേണ്ടതുമാണ്.",
+ "multimediaviewer-restriction-communist": "ഈ ചിത്രത്തിൽ ചില രാജ്യങ്ങളിൽ നിരോധിച്ചിട്ടുള്ള കമ്മ്യൂണിസ്റ്റ് മുദ്രണം ഉൾപ്പെടുന്നു.",
+ "multimediaviewer-restriction-costume": "ഈ ചിത്രത്തിൽ നിയമപ്രകാരമുള്ള പരിമിതപ്പെടുത്തലുകൾക്ക് ബാധകമായ വസ്ത്രധാരണം ഉണ്ടായേക്കാം.",
+ "multimediaviewer-restriction-currency": "ഈ ചിത്രം നിയമപ്രകാരമുള്ള പരിമിതപ്പെടുത്തലുകൾക്ക് ബാധകമായേക്കാവുന്ന ഒരു നാണയത്തിന്റേതാണ്.",
+ "multimediaviewer-restriction-design": "ഈ ചിത്രത്തിന്റെ വിഷയത്തിന്റെ രൂപകല്പന പകർപ്പവകാശസ്വതന്ത്രമല്ലാത്തതും നിയമപ്രകാരമുള്ള പരിമിതപ്പെടുത്തലുകൾക്ക് ബാധകമായതുമായേക്കാം.",
+ "multimediaviewer-restriction-fan-art": "ഈ ചിത്രം ഒരു ആരാധക-സൃഷ്ടി ആണ്, അതിനാലിതിന്റെ പുനരുപയോഗം നിയമപ്രകാരമുള്ള പരിമിതപ്പെടുത്തലിനു ബാധകമായേക്കാം.",
+ "multimediaviewer-restriction-ihl": "അന്താരാഷ്ട്ര മനുഷ്യാവകാശ നിയമപ്രകാരം പരിമിതപ്പെടുത്തിയിട്ടുള്ള ചിഹ്നങ്ങൾ ചിത്രത്തിൽ ഉണ്ട്.",
+ "multimediaviewer-restriction-insignia": "നിയമപ്രകാരമുള്ള പരിമിതപ്പെടുത്തലുകൾക്ക് ബാധകമായേക്കാവുന്ന ഔദ്യോഗിക മുദ്രണങ്ങൾ ഈ ചിത്രത്തിലുണ്ട്.",
+ "multimediaviewer-restriction-ita-mibac": "ഇറ്റാലിയൻ സാംസ്കാരിക പാരമ്പര്യത്തെ പ്രതിനിധീകരിക്കുന്ന ഒരു വസ്തുവിനെയാണ് ഈ ചിത്രത്തിൽ പുനഃസൃഷ്ടിക്കുന്നത്, അത് ഇറ്റാലിയൻ നിയമപ്രകാരം നിയന്ത്രിച്ചിട്ടുള്ളതാണ്.",
+ "multimediaviewer-restriction-nazi": "ഈ ചിത്രത്തിൽ ചില രാജ്യങ്ങളിൽ നിരോധിച്ചിട്ടുള്ള നാസി അല്ലെങ്കിൽ ഫാസിസ്റ്റ് മുദ്രണം ഉൾപ്പെടുന്നു.",
+ "multimediaviewer-restriction-personality": "ചിത്രത്തിന്റെ അനുവാദമില്ലാതെയുള്ള ചില പുനരുപയോഗങ്ങൾ അവരവരുടെ അവകാശപ്രകാരം നിയമപ്രകാരം നിയന്ത്രിച്ചിട്ടുള്ള വ്യക്തികൾ ഈ ചിത്രത്തിൽ ഉൾപ്പെടുന്നു.",
+ "multimediaviewer-restriction-trademarked": "ഈ ചിത്രത്തിൽ വ്യാപാരമുദ്രാ നിയമങ്ങൾ ബാധകമായ ഉള്ളടക്കം ഉണ്ടായേക്കാം.",
+ "multimediaviewer-restriction-default": "പകർപ്പവകാശനിയമത്തിൽ ഉൾപ്പെടാത്ത നിയമവ്യവസ്ഥകളാൽ ഈ ചിത്രം നിയന്ത്രിക്കപ്പെട്ടിട്ടുണ്ടാകാം. കൂടുതൽ വിവരങ്ങൾക്ക് പ്രമാണത്തിന്റെ വിവരണം കാണുക.",
+ "multimediaviewer-restriction-default-and-others": "പകർപ്പവകാശനിയമത്തിൽ ഉൾപ്പെടാത്ത നിയമവ്യവസ്ഥകളാൽ ഈ ചിത്രം കൂടുതൽ നിയന്ത്രിക്കപ്പെട്ടിട്ടുണ്ടാകാം. കൂടുതൽ വിവരങ്ങൾക്ക് പ്രമാണത്തിന്റെ വിവരണം കാണുക.",
+ "multimediaviewer-about-mmv": "വിവരണം",
+ "multimediaviewer-discuss-mmv": "സംവാദം",
+ "multimediaviewer-help-mmv": "സഹായം",
+ "multimediaviewer-optout-mmv": "മീഡിയ ദർശനോപാധി ഒഴിവാക്കുക",
+ "multimediaviewer-optin-mmv": "മീഡിയ ദർശനോപാധി സജ്ജമാക്കുക",
+ "multimediaviewer-optout-pending-mmv": "മീഡിയ ദർശനോപാധി ഒഴിവാക്കുന്നു",
+ "multimediaviewer-optin-pending-mmv": "മീഡിയ ദർശനോപാധി സജ്ജമാക്കുന്നു",
+ "multimediaviewer-optout-help": "ചിത്രങ്ങൾ പ്രദർശിപ്പിക്കാൻ മീഡിയ ദർശനോപാധി ഉപയോഗിക്കുന്നതല്ല. വീണ്ടും ഉപയോഗിക്കുവാൻ ചിത്രത്തിനരികിലുള്ള \"{{int:multimediaviewer-view-expanded}}\" ഞെക്കുക. എന്നിട്ട് \"{{int:multimediaviewer-optin-mmv}}\" ഞെക്കുക.",
+ "multimediaviewer-optin-help": "ചിത്രങ്ങൾ പ്രദർശിപ്പിക്കാൻ മീഡിയ ദർശനോപാധി ഉപയോഗിക്കുന്നതാണ്.",
+ "multimediaviewer-geoloc-north": "വ",
+ "multimediaviewer-geoloc-east": "കി",
+ "multimediaviewer-geoloc-south": "തെ",
+ "multimediaviewer-geoloc-west": "പ",
+ "multimediaviewer-geolocation": "സ്ഥാനം: $1",
+ "multimediaviewer-reuse-link": "ഈ പ്രമാണം ഉൾപ്പെടുത്തുക അല്ലെങ്കിൽ പങ്ക് വെയ്ക്കുക",
+ "multimediaviewer-reuse-loading-placeholder": "ശേഖരിക്കുന്നു...",
+ "multimediaviewer-reuse-copy-share": "ഈ പ്രമാണം പങ്ക് വെയ്ക്കാനായി കണ്ണി തിരഞ്ഞെടുക്കുക, പകർത്തുക (പിന്തുണയുണ്ടെങ്കിൽ)",
+ "multimediaviewer-reuse-copy-embed": "ഈ പ്രമാണം എംബെഡ് ചെയ്യാനായി കോഡ് തിരഞ്ഞെടുക്കുക, പകർത്തുക (പിന്തുണയുണ്ടെങ്കിൽ)",
+ "multimediaviewer-share-tab": "പങ്ക് വെയ്ക്കുക",
+ "multimediaviewer-embed-tab": "എംബെഡ് ചെയ്യുക",
+ "multimediaviewer-download-link": "ഈ പ്രമാണം ഡൗൺലോഡ് ചെയ്യുക",
+ "multimediaviewer-download-preview-link-title": "ബ്രൗസറിൽ കാണുക",
+ "multimediaviewer-download-original-button-name": "യഥാർത്ഥ പ്രമാണം ഡൗൺലോഡ് ചെയ്യുക",
+ "multimediaviewer-download-small-button-name": "ചെറിയ വലിപ്പത്തിൽ ഡൗൺലോഡ് ചെയ്യുക",
+ "multimediaviewer-download-medium-button-name": "ഇടത്തരം വലിപ്പത്തിൽ ഡൗൺലോഡ് ചെയ്യുക",
+ "multimediaviewer-download-large-button-name": "കൂടിയ വലിപ്പത്തിൽ ഡൗൺലോഡ് ചെയ്യുക",
+ "multimediaviewer-link-to-page": "പ്രമാണത്തിന്റെ വിവരണതാളിലേയ്ക്കുള്ള കണ്ണി",
+ "multimediaviewer-link-to-file": "യഥാർത്ഥ പ്രമാണത്തിലേയ്ക്കുള്ള കണ്ണി",
+ "multimediaviewer-share-explanation": "കണ്ണി പകർത്തുക, സ്വതന്ത്രമായി പങ്ക് വെയ്ക്കുക",
+ "multimediaviewer-embed-wt": "വിക്കിഎഴുത്ത്",
+ "multimediaviewer-embed-html": "എച്ച്.റ്റി.എം.എൽ.",
+ "multimediaviewer-embed-explanation": "പ്രമാണം എംബെഡ് ചെയ്യാനായി ഈ കോഡ് ഉപയോഗിക്കുക",
+ "multimediaviewer-html-embed-credit-link-text": "കണ്ണി",
+ "multimediaviewer-embed-byline": "സൃഷ്ടിച്ചത് $1",
+ "multimediaviewer-embed-license": "അനുമതി നൽകിയിരിക്കുന്നത് $1 പ്രകാരം ആണ്.",
+ "multimediaviewer-embed-via": "$1 വഴി.",
+ "multimediaviewer-default-embed-dimensions": "ലഘുചിത്രത്തിന് സ്വതേയുള്ള വലിപ്പം",
+ "multimediaviewer-original-embed-dimensions": "യഥാർത്ഥ പ്രമാണം $1",
+ "multimediaviewer-large-embed-dimensions": "കൂടിയ വലിപ്പം $1",
+ "multimediaviewer-medium-embed-dimensions": "ഇടത്തരം വലിപ്പം $1",
+ "multimediaviewer-small-embed-dimensions": "ചെറിയ വലിപ്പം $1",
+ "multimediaviewer-embed-dimensions": "$1 × $2 ബിന്ദു",
+ "multimediaviewer-description-page-button-text": "ഈ പ്രമാണത്തെക്കുറിച്ചുള്ള കൂടുതൽ വിവരങ്ങൾ",
+ "multimediaviewer-description-page-popup-text": "$1 സംരംഭത്തിൽ ഈ പ്രമാണത്തെക്കുറിച്ചുള്ള കൂടുതൽ വിവരങ്ങൾ",
+ "multimediaviewer-commons-subtitle": "സ്വതന്ത്ര മീഡിയ ശേഖരം",
+ "multimediaviewer-view-expanded": "മീഡിയ ദർശനോപാധിയിൽ കാണുക",
+ "multimediaviewer-view-config": "ക്രമീകരണങ്ങൾ",
+ "multimediaviewer-close-popup-text": "ഈ ഉപകരണം അടയ്ക്കുക (Esc)",
+ "multimediaviewer-fullscreen-popup-text": "മുഴുവൻ സ്ക്രീനിൽ കാണിക്കുക",
+ "multimediaviewer-defullscreen-popup-text": "മുഴുവൻ സ്ക്രീനിൽ നിന്ന് പുറത്തു കടക്കുക",
+ "multimediaviewer-next-image-alt-text": "അടുത്ത ചിത്രം കാണിക്കുക",
+ "multimediaviewer-prev-image-alt-text": "മുമ്പത്തെ ചിത്രം കാണിക്കുക",
+ "multimediaviewer-title-popup-text": "വിവരണം",
+ "multimediaviewer-credit-popup-text": "സ്രഷ്ടാവും സ്രോതസ്സ് വിവരങ്ങളും",
+ "multimediaviewer-title-popup-text-more": "മുഴുവൻ വിവരവും കാണുക",
+ "multimediaviewer-credit-popup-text-more": "സ്രഷ്ടാവിന്റെയും സ്രോതസ്സിന്റെയും മുഴുവൻ വിവരവും കാണുക",
+ "multimediaviewer-download-attribution-cta-header": "താങ്കൾ സ്രഷ്ടാവിന് കടപ്പാട് കൊടുക്കേണ്ടതുണ്ട്",
+ "multimediaviewer-download-optional-attribution-cta-header": "താങ്കൾ സ്രഷ്ടാവിന് കടപ്പാട് കൊടുക്കാവുന്നതാണ്",
+ "multimediaviewer-download-attribution-cta": "എങ്ങനെയാണെന്ന് കാണിച്ചു തരിക",
+ "multimediaviewer-download-attribution-copy": "ഈ പ്രമാണത്തിന്റെ കടപ്പാട് എഴുത്ത് തിരഞ്ഞെടുക്കുക, പകർത്തുക (പിന്തുണയുണ്ടെങ്കിൽ)",
+ "multimediaviewer-reuse-warning-deletion": "ഈ പ്രമാണം മായ്ച്ചാലോ എന്നയാലോചനയിലാണ്.",
+ "multimediaviewer-reuse-warning-nonfree": "ഈ പ്രമാണത്തിന് ഒരു സ്വതന്ത്ര ഉപയോഗാനുമതി ഇല്ല.",
+ "multimediaviewer-reuse-warning-noattribution": "ഈ പ്രമാണത്തിന് യാതൊരു കടപ്പാട് വിവരങ്ങളും ഇല്ല.",
+ "multimediaviewer-reuse-warning-generic": "ഉപയോഗിക്കുന്നതിന്റെ മുമ്പ് [$1 ഇതിന്റെ വിശദാംശങ്ങൾ] പരിശോധിക്കുക.",
+ "multimediaviewer-attr-plain": "വെറും എഴുത്ത്",
+ "multimediaviewer-attr-html": "എച്ച്.റ്റി.എം.എൽ.",
+ "multimediaviewer-options-tooltip": "മീഡിയ ദർശനോപാധി സജ്ജമാക്കുക അല്ലെങ്കിൽ ഒഴിവാക്കുക",
+ "multimediaviewer-options-dialog-header": "മീഡിയ ദർശനോപാധി ഒഴിവാക്കണോ?",
+ "multimediaviewer-options-text-header": "എല്ലാ പ്രമാണങ്ങളിൽ നിന്നും ഈ കാണൽ സൗകര്യം ഒഴിവാക്കുക.",
+ "multimediaviewer-options-text-body": "പ്രമാണത്തിന്റെ വിവരണ താളിൽ നിന്ന് ഇത് പീന്നീട് താങ്കൾക്ക് സജ്ജമാക്കാവുന്നതാണ്.",
+ "multimediaviewer-options-learn-more": "കൂടുതൽ അറിയുക",
+ "multimediaviewer-option-submit-button": "മീഡിയ ദർശനോപാധി ഒഴിവാക്കുക",
+ "multimediaviewer-option-cancel-button": "റദ്ദാക്കുക",
+ "multimediaviewer-disable-confirmation-header": "താങ്കൾ മീഡിയ ദർശനോപാധി ഒഴിവാക്കി",
+ "multimediaviewer-disable-confirmation-text": "അടുത്ത പ്രാവശ്യം $1 സംരംഭത്തിൽ ലഘുചിത്രത്തിൽ താങ്കൾ ഞെക്കുമ്പോൾ, താങ്കൾ നേരിട്ട് പ്രമാണത്തിന്റെ വിവരണതാളിലേക്കാവും ചെല്ലുക.",
+ "multimediaviewer-enable-dialog-header": "മീഡിയ ദർശനോപാധി സജ്ജമാക്കണോ?",
+ "multimediaviewer-enable-text-header": "ഈ മീഡിയ കാണൽ സൗകര്യം എല്ലാ പ്രമാണങ്ങൾക്കുമായി സ്വതേയുള്ളതായി സജ്ജമാക്കുക.",
+ "multimediaviewer-enable-submit-button": "മീഡിയ ദർശനോപാധി സജ്ജമാക്കുക",
+ "multimediaviewer-enable-confirmation-header": "എല്ലാ പ്രമാണങ്ങൾക്കും താങ്കൾ മീഡിയ ദർശനോപാധി സജ്ജമാക്കി",
+ "multimediaviewer-enable-confirmation-text": "അടുത്ത പ്രാവശ്യം $1 സംരംഭത്തിൽ ലഘുചിത്രത്തിൽ താങ്കൾ ഞെക്കുമ്പോൾ, മീഡിയ ദർശനോപാധി ഉപയോഗിക്കപ്പെടുന്നതാണ്.",
+ "multimediaviewer-enable-alert": "മീഡിയ ദർശനോപാധി ഇപ്പോൾ പ്രവർത്തനരഹിതമാണ്",
+ "multimediaviewer-disable-info-title": "താങ്കൾ മീഡിയ ദർശനോപാധി പ്രവർത്തനരഹിതമാക്കി",
+ "multimediaviewer-disable-info": "താങ്കൾക്ക് ഇപ്പോഴും ഒറ്റയൊറ്റ പ്രമാണങ്ങൾ മീഡിയ ദർശനോപാധി ഉപയോഗിച്ച് കാണാവുന്നതാണ്."
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/mn.json b/www/wiki/extensions/MultimediaViewer/i18n/mn.json
new file mode 100644
index 00000000..398c3070
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/mn.json
@@ -0,0 +1,10 @@
+{
+ "@metadata": {
+ "authors": [
+ "Wisdom",
+ "Munkhzaya.E"
+ ]
+ },
+ "multimediaviewer-discuss-mmv": "Хэлэлцэх",
+ "multimediaviewer-optin-help": "Зураг харуулахдаа Media Viewer -ыг ашиглана"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/mr.json b/www/wiki/extensions/MultimediaViewer/i18n/mr.json
new file mode 100644
index 00000000..cd777d75
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/mr.json
@@ -0,0 +1,16 @@
+{
+ "@metadata": {
+ "authors": [
+ "V.narsikar"
+ ]
+ },
+ "multimediaviewer-optin-pref": "<span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About मिडिया व्ह्यूअर]</span> सक्षम करा",
+ "multimediaviewer-thumbnail-error-report": "याबाबतचा अहवाल द्या",
+ "multimediaviewer-license-pd": "सार्वजनिक अधिक्षेत्र",
+ "multimediaviewer-optin-mmv": "माध्यम दर्शक(मिडिया व्ह्यूअर) सक्षम करा",
+ "multimediaviewer-text-embed-credit-text-bl": "$1,$2,$3 द्वारे",
+ "multimediaviewer-text-embed-credit-text-b": "$1,$2 द्वारे",
+ "multimediaviewer-html-embed-credit-text-bl": "$1,$2,$3 द्वारे",
+ "multimediaviewer-html-embed-credit-text-b": "$1,$2 द्वारे",
+ "multimediaviewer-html-embed-credit-link-text": "दुवा"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/ms.json b/www/wiki/extensions/MultimediaViewer/i18n/ms.json
new file mode 100644
index 00000000..f965a167
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/ms.json
@@ -0,0 +1,24 @@
+{
+ "@metadata": {
+ "authors": [
+ "Anakmalaysia"
+ ]
+ },
+ "multimediaviewer-optin-pref": "Hidupkan <span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About Media Viewer]</span>",
+ "multimediaviewer-repository-local": "Maklumat lanjut",
+ "multimediaviewer-credit-fallback": "Lihat maklumat pengarang",
+ "multimediaviewer-license-pd": "Domain Awam",
+ "multimediaviewer-reuse-link": "Sebarkan atau terapkan fail ini",
+ "multimediaviewer-download-link": "Muat turun fail ini",
+ "multimediaviewer-view-expanded": "Buka dalam Media Viewer",
+ "multimediaviewer-options-tooltip": "Hidupkan atau matikan Media Viewer",
+ "multimediaviewer-options-dialog-header": "Matikan Media Viewer?",
+ "multimediaviewer-option-submit-button": "Matikan Media Viewer",
+ "multimediaviewer-option-cancel-button": "Batalkan",
+ "multimediaviewer-disable-confirmation-header": "Anda telah mamtikan Media Viewer",
+ "multimediaviewer-enable-dialog-header": "Hidupkan Media Viewer?",
+ "multimediaviewer-enable-text-header": "Hidupkan ciri-ciri tayangan media untuk semua fail secara asal.",
+ "multimediaviewer-enable-submit-button": "Hidupkan Media Viewer",
+ "multimediaviewer-enable-confirmation-header": "Anda telah menghidupkan Media Viewer untuk semua fail",
+ "multimediaviewer-disable-info-title": "Anda telah mematikan Media Viewer"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/mwl.json b/www/wiki/extensions/MultimediaViewer/i18n/mwl.json
new file mode 100644
index 00000000..8725a264
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/mwl.json
@@ -0,0 +1,12 @@
+{
+ "@metadata": {
+ "authors": [
+ "MokaAkashiyaPT",
+ "Athena in Wonderland"
+ ]
+ },
+ "multimediaviewer-pref": "Bisualizador Multimédia",
+ "multimediaviewer-optin-pref": "Atibar l <span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About Bisualizador Multimédia]</span>",
+ "multimediaviewer-about-mmv": "Subre",
+ "multimediaviewer-optin-mmv": "Atibar l Bisualizador Multimédia"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/my.json b/www/wiki/extensions/MultimediaViewer/i18n/my.json
new file mode 100644
index 00000000..ed3196fc
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/my.json
@@ -0,0 +1,10 @@
+{
+ "@metadata": {
+ "authors": [
+ "Ninjastrikers"
+ ]
+ },
+ "multimediaviewer-repository-local": "နောက်ထပ် အချက်အလက်များ",
+ "multimediaviewer-help-mmv": "အကူအညီ",
+ "multimediaviewer-description-page-popup-text": "$1 ရှိ ဤဖိုင်အကြောင်း နောက်ထပ် အချက်အလက်များ"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/nan.json b/www/wiki/extensions/MultimediaViewer/i18n/nan.json
new file mode 100644
index 00000000..bedd3504
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/nan.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "唐吉訶德的侍從"
+ ]
+ },
+ "multimediaviewer-view-expanded": "Īng muî-thé kiám-sī-khì lâi khoàⁿ"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/nap.json b/www/wiki/extensions/MultimediaViewer/i18n/nap.json
new file mode 100644
index 00000000..7a80f0d2
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/nap.json
@@ -0,0 +1,11 @@
+{
+ "@metadata": {
+ "authors": [
+ "Chelin",
+ "C.R."
+ ]
+ },
+ "multimediaviewer-repository-local": "Cchiù dettaglie",
+ "multimediaviewer-license-pd": "Pubbreco duminio",
+ "multimediaviewer-optin-help": "Media Viewer sarrà 'o visore pe' mmustà l'immaggene"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/nb.json b/www/wiki/extensions/MultimediaViewer/i18n/nb.json
new file mode 100644
index 00000000..90aef10b
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/nb.json
@@ -0,0 +1,131 @@
+{
+ "@metadata": {
+ "authors": [
+ "Cocu",
+ "Danmichaelo",
+ "Jon Harald Søby"
+ ]
+ },
+ "multimediaviewer-desc": "Utvider miniatyrbilder til større størrelse i et fullskjermoverlegg.",
+ "multimediaviewer-pref": "Mediefremviser",
+ "multimediaviewer-pref-desc": "Forbedre multimediavisningsopplevelsen med dette nye verktøyet. Det viser bilder i større størrelse på sider som har miniatyrbilder. Bilder vises i et overlegg, som også kan utvides til fullskjerm.",
+ "multimediaviewer-optin-pref": "Aktiver <span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About mediefremviseren]</span>",
+ "multimediaviewer-file-page": "Gå til tilsvarende filside",
+ "multimediaviewer-repository-local": "Flere detaljer",
+ "multimediaviewer-datetime-created": "Opprettet: $1",
+ "multimediaviewer-datetime-uploaded": "Lastet opp: $1",
+ "multimediaviewer-credit-fallback": "Vis info om opphavsperson",
+ "multimediaviewer-multiple-authors": "{{PLURAL:$1|én annen forfatter|$1 andre forfattere}}",
+ "multimediaviewer-multiple-authors-combine": "$1 og $2",
+ "multimediaviewer-metadata-error": "Klarte ikke laste bildedetaljer (feil: $1)",
+ "multimediaviewer-thumbnail-error": "Beklager, filen kunne ikke vises",
+ "multimediaviewer-thumbnail-error-description": "Det ser ut til å være et teknisk problem. Du kan $1 eller $3 om det fortsetter. Feil: $2",
+ "multimediaviewer-thumbnail-error-retry": "prøve igjen",
+ "multimediaviewer-thumbnail-error-report": "rapportere problemet",
+ "multimediaviewer-license-cc-pd": "Offentlig eiendom",
+ "multimediaviewer-license-pd": "Offentlig eiendom",
+ "multimediaviewer-license-default": "Vis lisens",
+ "multimediaviewer-permission-title": "Detaljer om tillatelse for bruk",
+ "multimediaviewer-permission-link": "vis vilkår",
+ "multimediaviewer-permission-link-hide": "skjul vilkår",
+ "multimediaviewer-permission-viewmore": "Vis mer",
+ "multimediaviewer-restriction-2257": "Dette bildet inneholder seksuelt innhold som kan dekkes av Child Protection and Obscenity Enforcement Act i USA.",
+ "multimediaviewer-restriction-aus-reserve": "Dette bildet ble tatt i et Australian Commonwealth-reservat, og kan ikke brukes kommersielt uten tillatelse.",
+ "multimediaviewer-restriction-communist": "Dette bildet inneholder kommunistiske symboler som kan være bannlyste i enkelte land.",
+ "multimediaviewer-restriction-costume": "Dette bildet viser kostymer og kan ha juridiske begrensninger.",
+ "multimediaviewer-restriction-currency": "Dette bildet viser en valutaenhet og kan ha juridiske begrensninger.",
+ "multimediaviewer-restriction-design": "Designet til subjektet av dette bildet kan være under opphavsrett og ha juridiske begrensninger.",
+ "multimediaviewer-restriction-fan-art": "Dette bildet er fan-kunst, og gjenbruk kan være ha juridiske begrensninger.",
+ "multimediaviewer-restriction-ihl": "Dette bildet inneholder symboler som begrenses av internasjonal menneskerett.",
+ "multimediaviewer-restriction-insignia": "Dette bildet inneholder offisielle symboler som kan ha juridiske begrensninger.",
+ "multimediaviewer-restriction-ita-mibac": "Dette bildet viser en eiendom som tilhører italiensk kulturarv, og kan ha juridiske begrensninger i italiensk lov.",
+ "multimediaviewer-restriction-nazi": "Dette bildet inneholder nazistiske eller fascistiske symboler som kan være bannlyste i enkelte land.",
+ "multimediaviewer-restriction-personality": "Dette bildet inneholder personer som kan ha rettigheter som legger juridiske begrensninger på gjenbruk uten deres samtykke.",
+ "multimediaviewer-restriction-trademarked": "Dette bildet inneholder innhold som kan være gjenstand for varemerkelover.",
+ "multimediaviewer-restriction-default": "Dette bildet kan være begrenset av juridiske begrensninger utenom opphavsrett. Se filbeskrivelsessiden for detaljer.",
+ "multimediaviewer-restriction-default-and-others": "Dette bildet kan ha videre begrensninger av juridiske bestemmelser utenom opphavsrett. Se filbeskrivelsessiden for detaljer.",
+ "multimediaviewer-about-mmv": "Om",
+ "multimediaviewer-discuss-mmv": "Diskusjon",
+ "multimediaviewer-help-mmv": "Hjelp",
+ "multimediaviewer-optout-mmv": "Deaktiver mediefremviseren",
+ "multimediaviewer-optin-mmv": "Aktiver mediefremviseren",
+ "multimediaviewer-optout-pending-mmv": "Deaktiverer mediefremviseren",
+ "multimediaviewer-optin-pending-mmv": "Aktiverer mediefremviseren",
+ "multimediaviewer-optout-help": "Mediefremviseren vil ikke lenger brukes for å vise bilder. Hvis du vil skru den på igjen, trykk på «{{int:multimediaviewer-view-expanded}}»-knappen ved siden av et bilde. Trykk deretter på «{{int:multimediaviewer-optin-mmv}}».",
+ "multimediaviewer-optin-help": "Mediefremviseren vil bli brukt til å vise bilder.",
+ "multimediaviewer-geoloc-east": "Ø",
+ "multimediaviewer-geoloc-west": "V",
+ "multimediaviewer-geolocation": "Sted: $1",
+ "multimediaviewer-reuse-link": "Del eller bygg inn denne filen",
+ "multimediaviewer-reuse-loading-placeholder": "Laster…",
+ "multimediaviewer-reuse-copy-share": "Velg og kopier lenka for å dele denne fila (om støttet)",
+ "multimediaviewer-reuse-copy-embed": "Velg og kopier koden for å bygge inn denne fila (om støttet)",
+ "multimediaviewer-share-tab": "Del",
+ "multimediaviewer-embed-tab": "Bygg inn",
+ "multimediaviewer-download-link": "Last ned denne filen",
+ "multimediaviewer-download-preview-link-title": "Vis i nettleser",
+ "multimediaviewer-download-original-button-name": "Last ned originalfil",
+ "multimediaviewer-download-small-button-name": "Last ned liten størrelse",
+ "multimediaviewer-download-medium-button-name": "Last ned medium størrelse",
+ "multimediaviewer-download-large-button-name": "Last ned stor størrelse",
+ "multimediaviewer-link-to-page": "Lenke til filbeskrivelsessiden",
+ "multimediaviewer-link-to-file": "Lenke til originalfilen",
+ "multimediaviewer-share-explanation": "Kopier og del lenken fritt",
+ "multimediaviewer-embed-wt": "Wikitekst",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-embed-explanation": "Bruk denne koden for å bygge inn filen",
+ "multimediaviewer-text-embed-credit-text-bl": "Av $1, $2, $3",
+ "multimediaviewer-text-embed-credit-text-b": "Av $1, $2",
+ "multimediaviewer-html-embed-credit-text-bl": "Av $1, $2, $3",
+ "multimediaviewer-html-embed-credit-text-b": "Av $1, $2",
+ "multimediaviewer-html-embed-credit-link-text": "Lenke",
+ "multimediaviewer-embed-byline": "Av $1",
+ "multimediaviewer-embed-license": "Lisensiert under $1.",
+ "multimediaviewer-embed-via": "Via $1.",
+ "multimediaviewer-default-embed-dimensions": "Standard miniatyrbildestørrelse",
+ "multimediaviewer-original-embed-dimensions": "Originalfil $1",
+ "multimediaviewer-large-embed-dimensions": "Stor $1",
+ "multimediaviewer-medium-embed-dimensions": "Middels $1",
+ "multimediaviewer-small-embed-dimensions": "Liten $1",
+ "multimediaviewer-description-page-button-text": "Mer info om denne filen",
+ "multimediaviewer-description-page-popup-text": "Mer info om denne filen på $1",
+ "multimediaviewer-commons-subtitle": "Det frie mediearkivet",
+ "multimediaviewer-view-expanded": "Åpne i mediefremviseren",
+ "multimediaviewer-view-config": "Innstillinger",
+ "multimediaviewer-close-popup-text": "Lukk dette verktøyet (Esc)",
+ "multimediaviewer-fullscreen-popup-text": "Vis i fullskjerm",
+ "multimediaviewer-defullscreen-popup-text": "Lukk fullskjerm",
+ "multimediaviewer-next-image-alt-text": "Vis neste bilde",
+ "multimediaviewer-prev-image-alt-text": "Vis forrige bilde",
+ "multimediaviewer-title-popup-text": "Beskrivelse",
+ "multimediaviewer-credit-popup-text": "Informasjon om kilde og opphavsperson",
+ "multimediaviewer-title-popup-text-more": "Vis fullstendig beskrivelse",
+ "multimediaviewer-credit-popup-text-more": "Vis fullstendig informasjon om kilde og opphavsperson",
+ "multimediaviewer-download-attribution-cta-header": "Du må navngi opphavspersonen(e)",
+ "multimediaviewer-download-optional-attribution-cta-header": "Du kan navngi opphavspersonen",
+ "multimediaviewer-download-attribution-cta": "Vis meg hvordan",
+ "multimediaviewer-download-attribution-copy": "Velg og kopier (om støttet) attribusjonsteksten for denne fila",
+ "multimediaviewer-reuse-warning-deletion": "Denne filen vurderes slettet.",
+ "multimediaviewer-reuse-warning-nonfree": "Denne filen har ikke en fri lisens.",
+ "multimediaviewer-reuse-warning-noattribution": "Denne filen har ingen attribusjonsinformasjon.",
+ "multimediaviewer-reuse-warning-generic": "Sjekk [$1 detaljene dens] før du bruker den.",
+ "multimediaviewer-attr-plain": "Enkel tekst",
+ "multimediaviewer-options-tooltip": "Skru av eller på mediefremviseren",
+ "multimediaviewer-options-dialog-header": "Skru av mediefremviseren?",
+ "multimediaviewer-options-text-header": "Hopp over denne visningen som standard.",
+ "multimediaviewer-options-text-body": "Du kan aktivere den igjen på et senere tidspunkt fra fildetaljsidene.",
+ "multimediaviewer-options-learn-more": "Mer informasjon",
+ "multimediaviewer-option-submit-button": "Skru av mediefremviseren",
+ "multimediaviewer-option-cancel-button": "Avbryt",
+ "multimediaviewer-disable-confirmation-header": "Du har skrudd av mediefremviseren",
+ "multimediaviewer-disable-confirmation-text": "Neste gang du trykker på et miniatyrbilde på $1, vil du du bli sendt direkte til fildetaljene.",
+ "multimediaviewer-enable-dialog-header": "Aktiver mediefremviseren?",
+ "multimediaviewer-enable-text-header": "Aktiver denne funksjonen som standard for alle filer.",
+ "multimediaviewer-enable-submit-button": "Aktiver mediefremviseren",
+ "multimediaviewer-enable-confirmation-header": "Du har aktivert mediefremviseren for alle filer",
+ "multimediaviewer-enable-confirmation-text": "Mediefremviseren bli brukt neste gang du trykker på et miniatyrbilde på $1.",
+ "multimediaviewer-enable-alert": "Mediefremviseren er deaktivert som standard",
+ "multimediaviewer-disable-info-title": "Du har deaktivert mediefremviseren",
+ "multimediaviewer-disable-info": "Du kan fremdeles vise enkeltfiler i mediefremviseren.",
+ "multimediaviewer-errorreport-privacywarning": "Detaljer om feilen legges ved rapporten, og vil være synlige offentlig. Om du ikke er komfortabel med det kan du redigere rapporten nedenfor og fjerne data du ikke ønsker å dele."
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/nds-nl.json b/www/wiki/extensions/MultimediaViewer/i18n/nds-nl.json
new file mode 100644
index 00000000..f5c8a309
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/nds-nl.json
@@ -0,0 +1,11 @@
+{
+ "@metadata": {
+ "authors": [
+ "Servien"
+ ]
+ },
+ "multimediaviewer-help-mmv": "Hulpe",
+ "multimediaviewer-fullscreen-popup-text": "Op volledig scharm laoten zien",
+ "multimediaviewer-defullscreen-popup-text": "Volledig scharm sluten",
+ "multimediaviewer-title-popup-text": "Bestaandsnaam"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/ne.json b/www/wiki/extensions/MultimediaViewer/i18n/ne.json
new file mode 100644
index 00000000..e0f7d339
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/ne.json
@@ -0,0 +1,14 @@
+{
+ "@metadata": {
+ "authors": [
+ "NehalDaveND"
+ ]
+ },
+ "multimediaviewer-help-mmv": "सहायता",
+ "multimediaviewer-reuse-loading-placeholder": "लोड हुदैछ...",
+ "multimediaviewer-share-tab": "बाड्ने",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-title-popup-text": "वर्णन",
+ "multimediaviewer-options-learn-more": "जाने थप",
+ "multimediaviewer-option-cancel-button": "रद्द गर्ने"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/nl.json b/www/wiki/extensions/MultimediaViewer/i18n/nl.json
new file mode 100644
index 00000000..dc6e860d
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/nl.json
@@ -0,0 +1,107 @@
+{
+ "@metadata": {
+ "authors": [
+ "Arent",
+ "SPQRobin",
+ "Siebrand",
+ "Sjoerddebruin",
+ "Southparkfan",
+ "Romaine",
+ "Mathonius",
+ "Macofe",
+ "Robin0van0der0vliet",
+ "Robin van der Vliet",
+ "Mainframe98",
+ "Mar(c)"
+ ]
+ },
+ "multimediaviewer-desc": "Miniatuurafbeeldingen schermvullend weergeven.",
+ "multimediaviewer-pref": "MediaViewer",
+ "multimediaviewer-pref-desc": "Verbeter uw kijkervaring met deze nieuwe tool. Afbeeldingen worden groter weergegeven op pagina's met thumbnails. Afbeeldingen worden op een prettigere manier schermvullend weergegeven en kunnen ook op ware grootte worden bekeken.",
+ "multimediaviewer-optin-pref": "<span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About MediaViewer]</span> inschakelen",
+ "multimediaviewer-file-page": "Naar de bestandspagina gaan",
+ "multimediaviewer-repository-local": "Meer informatie",
+ "multimediaviewer-datetime-created": "Aangemaakt: $1",
+ "multimediaviewer-datetime-uploaded": "Geüpload: $1",
+ "multimediaviewer-credit-fallback": "Auteursgegevens bekijken",
+ "multimediaviewer-multiple-authors": "{{PLURAL:$1|nog een auteur|nog $1 meer auteurs}}",
+ "multimediaviewer-multiple-authors-combine": "$1 en $2",
+ "multimediaviewer-metadata-error": "Kan afbeeldingsgegevens niet laden (fout: $1)",
+ "multimediaviewer-thumbnail-error": "Sorry, het bestand kan niet worden weergeven",
+ "multimediaviewer-thumbnail-error-description": "Er is een technisch probleem. U kunt $1 of $3 als dit blijft voorkomen. Fout: $2",
+ "multimediaviewer-thumbnail-error-retry": "opnieuw proberen",
+ "multimediaviewer-thumbnail-error-report": "het probleem melden",
+ "multimediaviewer-license-cc-pd": "Publiek domein",
+ "multimediaviewer-license-pd": "Publiek domein",
+ "multimediaviewer-license-default": "Licentie weergeven",
+ "multimediaviewer-permission-title": "Toesteminggegevens",
+ "multimediaviewer-permission-link": "voorwaarden bekijken",
+ "multimediaviewer-permission-link-hide": "voorwaarden verbergen",
+ "multimediaviewer-permission-viewmore": "Meer weergeven",
+ "multimediaviewer-about-mmv": "Over",
+ "multimediaviewer-discuss-mmv": "Overleg",
+ "multimediaviewer-help-mmv": "Hulp",
+ "multimediaviewer-optout-mmv": "MediaViewer uitschakelen",
+ "multimediaviewer-optin-mmv": "MediaViewer inschakelen",
+ "multimediaviewer-optout-pending-mmv": "MediaViewer uitschakelen",
+ "multimediaviewer-optin-pending-mmv": "MediaViewer inschakelen",
+ "multimediaviewer-optin-help": "De MediaViewer wordt gebruikt voor het weergeven van afbeeldingen.",
+ "multimediaviewer-geolocation": "Locatie: $1",
+ "multimediaviewer-reuse-link": "Dit bestand delen of invoegen",
+ "multimediaviewer-reuse-loading-placeholder": "Bezig met laden...",
+ "multimediaviewer-share-tab": "Delen",
+ "multimediaviewer-embed-tab": "Invoegen",
+ "multimediaviewer-download-link": "Dit bestand downloaden",
+ "multimediaviewer-download-preview-link-title": "Weergeven in browser",
+ "multimediaviewer-download-original-button-name": "Oorspronkelijk bestand downloaden",
+ "multimediaviewer-download-small-button-name": "Klein formaat downloaden",
+ "multimediaviewer-download-medium-button-name": "Middelgroot formaat downloaden",
+ "multimediaviewer-download-large-button-name": "Groot formaat downloaden",
+ "multimediaviewer-link-to-page": "Koppeling naar de pagina met de bestandsbeschrijving",
+ "multimediaviewer-link-to-file": "Koppeling naar het originele bestand",
+ "multimediaviewer-share-explanation": "Kopiëren en delen",
+ "multimediaviewer-embed-wt": "Wikitekst",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-embed-explanation": "Gebruik deze code om het bestand ergens in te voegen",
+ "multimediaviewer-text-embed-credit-text-bl": "Door $1, $2, $3",
+ "multimediaviewer-text-embed-credit-text-b": "Door $1, $2",
+ "multimediaviewer-html-embed-credit-text-bl": "Door $1, $2, $3",
+ "multimediaviewer-html-embed-credit-text-b": "Door $1, $2",
+ "multimediaviewer-embed-byline": "Door $1",
+ "multimediaviewer-embed-license": "Licentie $1.",
+ "multimediaviewer-embed-via": "Via $1.",
+ "multimediaviewer-default-embed-dimensions": "Standaard miniatuurgrootte",
+ "multimediaviewer-original-embed-dimensions": "Oorspronkelijk bestand $1",
+ "multimediaviewer-large-embed-dimensions": "Groot $1",
+ "multimediaviewer-medium-embed-dimensions": "Middelgroot $1",
+ "multimediaviewer-small-embed-dimensions": "Klein $1",
+ "multimediaviewer-description-page-button-text": "Meer informatie over dit bestand",
+ "multimediaviewer-description-page-popup-text": "Meer informatie over dit bestand op $1",
+ "multimediaviewer-commons-subtitle": "De vrije mediaverzameling",
+ "multimediaviewer-view-expanded": "In MediaViewer openen",
+ "multimediaviewer-view-config": "Configuratie",
+ "multimediaviewer-close-popup-text": "Hulpmiddel sluiten (Esc)",
+ "multimediaviewer-fullscreen-popup-text": "In volledig scherm bekijken",
+ "multimediaviewer-defullscreen-popup-text": "Volledig schermmodus verlaten",
+ "multimediaviewer-next-image-alt-text": "Toon volgende afbeelding",
+ "multimediaviewer-prev-image-alt-text": "Toon vorige afbeelding",
+ "multimediaviewer-title-popup-text": "Beschrijving",
+ "multimediaviewer-credit-popup-text": "Auteur- en broninformatie",
+ "multimediaviewer-title-popup-text-more": "Volledige beschrijving bekijken",
+ "multimediaviewer-credit-popup-text-more": "Volledige auteur en bron bekijken",
+ "multimediaviewer-download-attribution-cta-header": "U moet aan naamsvermelding doen",
+ "multimediaviewer-download-optional-attribution-cta-header": "U kunt aan naamsvermelding doen",
+ "multimediaviewer-download-attribution-cta": "Laat mij zien hoe",
+ "multimediaviewer-attr-plain": "Zonder opmaak",
+ "multimediaviewer-options-tooltip": "MediaViewer in- of uitschakelen",
+ "multimediaviewer-options-dialog-header": "MediaViewer uitschakelen?",
+ "multimediaviewer-options-learn-more": "Meer lezen",
+ "multimediaviewer-option-submit-button": "MediaViewer uitschakelen",
+ "multimediaviewer-option-cancel-button": "Annuleren",
+ "multimediaviewer-disable-confirmation-header": "U hebt de MediaViewer uitgeschakeld",
+ "multimediaviewer-enable-dialog-header": "MediaViewer inschakelen?",
+ "multimediaviewer-enable-submit-button": "MediaViewer inschakelen",
+ "multimediaviewer-enable-alert": "MediaViewer is nu uitgeschakeld",
+ "multimediaviewer-disable-info-title": "U hebt de MediaViewer uitgeschakeld",
+ "multimediaviewer-disable-info": "U kunt nog steeds losse bestanden bekijken met de MediaViewer."
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/nn.json b/www/wiki/extensions/MultimediaViewer/i18n/nn.json
new file mode 100644
index 00000000..116856e6
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/nn.json
@@ -0,0 +1,92 @@
+{
+ "@metadata": {
+ "authors": [
+ "Njardarlogar"
+ ]
+ },
+ "multimediaviewer-desc": "Vis større storleikar av miniatyrbilete i eit fullskjermgrensesnitt.",
+ "multimediaviewer-pref": "Medieframsynar",
+ "multimediaviewer-pref-desc": "Betra medievisingsopplevinga di med dette nye verktøyet. Det viser fram bilete i større storleikar på sider som har miniatyrbilete. Bileta vert viste i eit betre overlagt fullskjermsgrensesnitt og kan òg visast som fullskjerm.",
+ "multimediaviewer-optin-pref": "Slå på <span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About medieframsynaren]</span>",
+ "multimediaviewer-file-page": "Gå til den tilhøyrande filsida",
+ "multimediaviewer-repository-local": "Fleire detaljar",
+ "multimediaviewer-datetime-created": "Oppretta: $1",
+ "multimediaviewer-datetime-uploaded": "Opplasta: $1",
+ "multimediaviewer-credit-fallback": "Sjå informasjon om opphavsperson",
+ "multimediaviewer-multiple-authors": "og {{PLURAL:$1|éin forfattar til|$1 andre forfattarar}}",
+ "multimediaviewer-metadata-error": "Feil: kunne ikkje lasta biletdata. $1",
+ "multimediaviewer-thumbnail-error": "Feil: kunne ikkje lasta miniatyrbiletdata. $1",
+ "multimediaviewer-license-cc-pd": "Offentleg eigedom",
+ "multimediaviewer-license-pd": "Offentleg eigedom",
+ "multimediaviewer-license-default": "Sjå lisens",
+ "multimediaviewer-permission-title": "Lisensdetaljar",
+ "multimediaviewer-permission-link": "sjå vilkår",
+ "multimediaviewer-permission-viewmore": "Sjå meir",
+ "multimediaviewer-about-mmv": "Om medieframsynaren",
+ "multimediaviewer-discuss-mmv": "Diskuter denne funksjonen",
+ "multimediaviewer-help-mmv": "Hjelp",
+ "multimediaviewer-optout-mmv": "Slå av medieframsynaren",
+ "multimediaviewer-optin-mmv": "Slå på medieframsynaren",
+ "multimediaviewer-optout-pending-mmv": "Slår av medieframsynaren",
+ "multimediaviewer-optin-pending-mmv": "Slår på medieframsynaren",
+ "multimediaviewer-optout-help": "Medieframsynaren vil ikkje brukast lenger for å visa bilete. For å bruka han att, klikk på «{{int:multimediaviewer-view-expanded}}»-knappen ved sida av eit kva som helst bilete. Klikk så på «{{int:multimediaviewer-optin-mmv}}».",
+ "multimediaviewer-optin-help": "Medieframsynaren vil brukast til å visa bilete.",
+ "multimediaviewer-geolocation": "Stad: $1",
+ "multimediaviewer-reuse-link": "Bruk eller del fila",
+ "multimediaviewer-reuse-loading-placeholder": "Lastar…",
+ "multimediaviewer-share-tab": "Del",
+ "multimediaviewer-embed-tab": "Inkluder",
+ "multimediaviewer-download-link": "Last ned fila",
+ "multimediaviewer-download-preview-link-title": "Vis i nettlesar",
+ "multimediaviewer-download-original-button-name": "Last ned opphavleg fil",
+ "multimediaviewer-download-small-button-name": "Last ned i liten storleik",
+ "multimediaviewer-download-medium-button-name": "Last ned i mellomstor storleik",
+ "multimediaviewer-download-large-button-name": "Last ned i stor storleik",
+ "multimediaviewer-link-to-page": "Lenkje til filskildringssida",
+ "multimediaviewer-link-to-file": "Lenkje til opphavleg fil",
+ "multimediaviewer-share-explanation": "Kopier lenkja og del henne fritt",
+ "multimediaviewer-embed-wt": "Wikitekst",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-embed-explanation": "Bruk denne koden for å inkludera fila",
+ "multimediaviewer-embed-byline": "Av $1",
+ "multimediaviewer-embed-license": "Lisensiert under $1.",
+ "multimediaviewer-embed-via": "Via $1.",
+ "multimediaviewer-default-embed-dimensions": "Vanleg storleik på minatyrbilete",
+ "multimediaviewer-original-embed-dimensions": "Opphavleg fil $1",
+ "multimediaviewer-large-embed-dimensions": "Stor $1",
+ "multimediaviewer-medium-embed-dimensions": "Mellomstor $1",
+ "multimediaviewer-small-embed-dimensions": "Liten $1",
+ "multimediaviewer-description-page-button-text": "Fleire detaljar om fila",
+ "multimediaviewer-description-page-popup-text": "Fleire detaljar om fila på $1",
+ "multimediaviewer-commons-subtitle": "Det frie medielageret",
+ "multimediaviewer-view-expanded": "Opna i medieframsynaren",
+ "multimediaviewer-view-config": "Innstillingar",
+ "multimediaviewer-close-popup-text": "Lat att verktøyet (Esc)",
+ "multimediaviewer-fullscreen-popup-text": "Vis som fullskjerm",
+ "multimediaviewer-defullscreen-popup-text": "Gå ut av fullskjermsmodus",
+ "multimediaviewer-title-popup-text": "Skildring",
+ "multimediaviewer-credit-popup-text": "Opphavsperson- og kjeldeinformasjon",
+ "multimediaviewer-title-popup-text-more": "Sjå heile skildringa",
+ "multimediaviewer-credit-popup-text-more": "Sjå forfattar og kjelde",
+ "multimediaviewer-download-attribution-cta-header": "Du må godskriva opphavspersonen",
+ "multimediaviewer-download-optional-attribution-cta-header": "Du kan godskriva opphavspersonen",
+ "multimediaviewer-download-attribution-cta": "Vis meg korleis",
+ "multimediaviewer-attr-plain": "Enkel",
+ "multimediaviewer-options-tooltip": "Slå av eller på medieframsynaren",
+ "multimediaviewer-options-dialog-header": "Slå av medieframsynaren?",
+ "multimediaviewer-options-text-header": "Ikkje nytt denne visingsmåten for nokon som helst filer.",
+ "multimediaviewer-options-text-body": "Du kan slå han på att seinare gjennom fildetaljsidene.",
+ "multimediaviewer-options-learn-more": "Les meir",
+ "multimediaviewer-option-submit-button": "Slå av medieframsynaren",
+ "multimediaviewer-option-cancel-button": "Bryt av",
+ "multimediaviewer-disable-confirmation-header": "Du har slege av medieframsynaren",
+ "multimediaviewer-disable-confirmation-text": "Du vert sendt rett til alle fildetaljane neste gongen du klikkar på ei miniatyrbilete på $1.",
+ "multimediaviewer-enable-dialog-header": "Slå på medieframsynaren?",
+ "multimediaviewer-enable-text-header": "Slå på denne medieframsyningsfunksjonen for alle filer som standard.",
+ "multimediaviewer-enable-submit-button": "Slå på medieframsynaren",
+ "multimediaviewer-enable-confirmation-header": "Du har slege på medieframsynaren for alle filer",
+ "multimediaviewer-enable-confirmation-text": "Medieframsynaren vert nytta neste gongen du klikkar på ei miniatyrbilete på $1.",
+ "multimediaviewer-enable-alert": "Medieframsynaren er no slegen av",
+ "multimediaviewer-disable-info-title": "Du har slege av medieframsynaren",
+ "multimediaviewer-disable-info": "Du kan framleis sjå einskildfiler med medieframsynaren."
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/oc.json b/www/wiki/extensions/MultimediaViewer/i18n/oc.json
new file mode 100644
index 00000000..043fd1d2
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/oc.json
@@ -0,0 +1,42 @@
+{
+ "@metadata": {
+ "authors": [
+ "Cedric31"
+ ]
+ },
+ "multimediaviewer-desc": "Agrandís las vinhetas dins una interfàcia en ecran complet.",
+ "multimediaviewer-pref": "Visionadoira de Mèdias",
+ "multimediaviewer-pref-desc": "Melhoratz vòstra experiéncia de visualizacion multimèdia amb aquesta aisina novèla. Aficha los imatges en granda talha sus las paginas qu'an de vinhetas. Los imatges son afichats dins un polit quadre d’interfàcia en ecran complet, e tanben, se pòdon afichar en talha maximala.",
+ "multimediaviewer-file-page": "Anar a la pagina del fichièr correspondent",
+ "multimediaviewer-repository-local": "Mai de detalhs",
+ "multimediaviewer-datetime-created": "Creacion : $1",
+ "multimediaviewer-datetime-uploaded": "Mandat : $1",
+ "multimediaviewer-license-cc-pd": "Domeni public",
+ "multimediaviewer-license-pd": "Domeni public",
+ "multimediaviewer-license-default": "Afichar la licéncia",
+ "multimediaviewer-permission-title": "Detalhs dels dreits",
+ "multimediaviewer-permission-link": "afichar las condicions",
+ "multimediaviewer-permission-viewmore": "Veire mai",
+ "multimediaviewer-about-mmv": "A prepaus",
+ "multimediaviewer-discuss-mmv": "Discussion",
+ "multimediaviewer-help-mmv": "Ajuda",
+ "multimediaviewer-geolocation": "Emplaçament : $1",
+ "multimediaviewer-reuse-link": "Partejar o inclure aqueste fichièr",
+ "multimediaviewer-reuse-loading-placeholder": "Cargament en cors…",
+ "multimediaviewer-download-link": "Telecargar aqueste fichièr",
+ "multimediaviewer-download-preview-link-title": "Afichar dins lo navigador",
+ "multimediaviewer-download-original-button-name": "Telecargar lo fichièr d’origina",
+ "multimediaviewer-download-small-button-name": "Telecargar en pichona talha",
+ "multimediaviewer-download-medium-button-name": "Telecargar en talha mejana",
+ "multimediaviewer-download-large-button-name": "Telecargar en granda talha",
+ "multimediaviewer-default-embed-dimensions": "Talha de vinheta per defaut",
+ "multimediaviewer-original-embed-dimensions": "Fichièr d’origina $1",
+ "multimediaviewer-large-embed-dimensions": "Grand $1",
+ "multimediaviewer-medium-embed-dimensions": "Mejan $1",
+ "multimediaviewer-small-embed-dimensions": "Pichon $1",
+ "multimediaviewer-description-page-button-text": "Mai de detalhs sus aqueste fichièr",
+ "multimediaviewer-description-page-popup-text": "Mai de detalhs sus aqueste fichièr en $1",
+ "multimediaviewer-commons-subtitle": "L'entrepaus de mèdia liure",
+ "multimediaviewer-view-expanded": "Dobrir dins lo Visualizador de mèdias",
+ "multimediaviewer-close-popup-text": "Tampar aquesta aisina (Esc)"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/om.json b/www/wiki/extensions/MultimediaViewer/i18n/om.json
new file mode 100644
index 00000000..79818d7b
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/om.json
@@ -0,0 +1,9 @@
+{
+ "@metadata": {
+ "authors": [
+ "Tumsaa"
+ ]
+ },
+ "multimediaviewer-about-mmv": "Waa'ee",
+ "multimediaviewer-discuss-mmv": "Marii"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/or.json b/www/wiki/extensions/MultimediaViewer/i18n/or.json
new file mode 100644
index 00000000..3b7901bd
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/or.json
@@ -0,0 +1,55 @@
+{
+ "@metadata": {
+ "authors": [
+ "Psubhashish"
+ ]
+ },
+ "multimediaviewer-desc": "ପୂରା ସ୍କ୍ରିନରେ ଥମ୍ବନେଲକୁ ବଡ଼ ଆକାର କରିଦିଅନ୍ତୁ ।",
+ "multimediaviewer-pref": "ମିଡ଼ିଆ ଭିଉଅର",
+ "multimediaviewer-pref-desc": "ଏହି ଟୁଲ ବ୍ୟବହାର କରି ଆପଣ ନିଜର ମଲ୍ଟିମିଡ଼ିଆରେ ଦେଖିବାର ଅନୁଭୂତି ବଢ଼ାଇପାରିବେ । ଥମ୍ବନେଲ ଥିବା ପୃଷ୍ଠାମାନଙ୍କରେ ଏହା ଛବିଗୁଡ଼ିକୁ ବଡ଼ କରି ଦେଖାଇଥାଏ । ଏଥିରେ ଛବିସବୁ ସୁନ୍ଦରଭାବେ ପୂରା ସ୍କ୍ରିନରେ ଦେଖିପାରିବେ, ଏବଂ ପୂରା ସ୍କ୍ରିନରେ ମଧ୍ୟ ଦେଖିପାରିବେ ।",
+ "multimediaviewer-optin-pref": "ନୂଆ ମିଡ଼ିଆ ଦେଖିବା ସକ୍ରିୟ କରନ୍ତୁ",
+ "multimediaviewer-file-page": "ଏଥି ସମ୍ବନ୍ଧିତ ଫାଇଲ ପୃଷ୍ଠାକୁ ଯାଆନ୍ତୁ",
+ "multimediaviewer-repository-local": "ଅଧିକ ଜାଣନ୍ତୁ",
+ "multimediaviewer-datetime-created": "$1ରେ ତିଆରି ହେଲା",
+ "multimediaviewer-datetime-uploaded": "$1ରେ ଅପଲୋଡ଼ ହେଲା",
+ "multimediaviewer-metadata-error": "ଚେତାବନୀ: ଛବିର ତଥ୍ୟ ଲୋଡ଼ ହୋଇପାରିଲା ନାହିଁ । $1",
+ "multimediaviewer-thumbnail-error": "ଚେତାବନୀ: ଥମ୍ବନେଲ ତଥ୍ୟ ଲୋଡ଼ ହୋଇପାରିଲା ନାହିଁ । $1",
+ "multimediaviewer-license-cc-pd": "ପବ୍ଲିକ ଡୋମେନ",
+ "multimediaviewer-license-pd": "ପବ୍ଲିକ ଡୋମେନ",
+ "multimediaviewer-license-default": "ଲାଇସେନ୍ସ ଦେଖନ୍ତୁ",
+ "multimediaviewer-permission-title": "ଲାଇସେନ୍ସ ସବିଶେଷ",
+ "multimediaviewer-permission-link": "ସର୍ତ୍ତାବଳୀ ଦେଖନ୍ତୁ",
+ "multimediaviewer-permission-viewmore": "ଅଧିକ ଦେଖନ୍ତୁ",
+ "multimediaviewer-about-mmv": "ମିଡ଼ିଆ ଭିଉଅର ବାବଦରେ",
+ "multimediaviewer-discuss-mmv": "ଏହି ବୈଶିଷ୍ଠଟି ଆଲୋଚନା କରନ୍ତୁ",
+ "multimediaviewer-help-mmv": "ସହଯୋଗ",
+ "multimediaviewer-geolocation": "ଅବସ୍ଥିତି: $1",
+ "multimediaviewer-reuse-link": "ଏହି ଫାଇଲ ବ୍ୟବହାର କରନ୍ତୁ",
+ "multimediaviewer-reuse-loading-placeholder": "ଖୋଲୁଅଛି...",
+ "multimediaviewer-share-tab": "ବିତରଣ କରନ୍ତୁ",
+ "multimediaviewer-embed-tab": "ଏମବେଡ଼",
+ "multimediaviewer-download-link": "ଡାଉନଲୋଡ଼",
+ "multimediaviewer-download-preview-link-title": "ବ୍ରାଉଜରରେ ଦେଖନ୍ତୁ",
+ "multimediaviewer-download-original-button-name": "ମୂଳ ଆକାର ଡାଉନଲୋଡ଼ କରନ୍ତୁ",
+ "multimediaviewer-download-small-button-name": "ଛୋଟ ଆକାର ଡାଉନଲୋଡ଼ କରନ୍ତୁ",
+ "multimediaviewer-download-medium-button-name": "ମଧ୍ୟମ ଆକାର ଡାଉନଲୋଡ଼ କରନ୍ତୁ",
+ "multimediaviewer-download-large-button-name": "ବଡ଼ ଆକାର ଡାଉନଲୋଡ଼ କରନ୍ତୁ",
+ "multimediaviewer-link-to-page": "ବିବରଣ ପୃଷ୍ଠା ସହିତ ଲିଙ୍କ କରନ୍ତୁ",
+ "multimediaviewer-link-to-file": "ମୂଳ ପୃଷ୍ଠା ସହିତ ଲିଙ୍କ କରନ୍ତୁ",
+ "multimediaviewer-share-explanation": "ନକଲ କରି ଖୋଲାରେ ଏହି ଲିଙ୍କଟି ବିତରଣ କରନ୍ତୁ",
+ "multimediaviewer-embed-wt": "ଉଇକିଟେକ୍ସଟ",
+ "multimediaviewer-embed-html": "ଏଚଟିଏମଏଲ",
+ "multimediaviewer-embed-explanation": "ଫାଇଲଟି ଏମବେଡ଼ କରିବା ପାଇଁ ଏହି କୋଡ଼ ଦିଅନ୍ତୁ",
+ "multimediaviewer-embed-byline": "$1ଙ୍କ ଦ୍ୱାରା",
+ "multimediaviewer-embed-license": "$1 ଲାଇସେନ୍ସରେ ପ୍ରକାଶିତ ।",
+ "multimediaviewer-embed-via": "$1ଙ୍କ ଦ୍ୱାରା",
+ "multimediaviewer-default-embed-dimensions": "ଡିଫଲ୍ଟ ଥମ୍ବନେଲ ଆକାର",
+ "multimediaviewer-original-embed-dimensions": "ମୂଳ ଆକାର $1",
+ "multimediaviewer-large-embed-dimensions": "ଆକାର $1",
+ "multimediaviewer-medium-embed-dimensions": "ମଧ୍ୟମ $1",
+ "multimediaviewer-small-embed-dimensions": "ଛୋଟ $1",
+ "multimediaviewer-description-page-button-text": "ସବିଶେଷ",
+ "multimediaviewer-description-page-popup-text": "$1 ବାବଦରେ ଅଧିକ ସବିଶେଷ",
+ "multimediaviewer-commons-subtitle": "ଖୋଲା ମିଡ଼ିଆ ଭଣ୍ଡାରଟିଏ",
+ "multimediaviewer-view-expanded": "ବଡ଼କରି ଦେଖାଇବେ"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/pa.json b/www/wiki/extensions/MultimediaViewer/i18n/pa.json
new file mode 100644
index 00000000..5a9d834f
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/pa.json
@@ -0,0 +1,10 @@
+{
+ "@metadata": {
+ "authors": [
+ "Babanwalia"
+ ]
+ },
+ "multimediaviewer-repository-local": "ਹੋਰ ਵੇਰਵਾ",
+ "multimediaviewer-download-link": "ਇਹ ਫ਼ਾਈਲ ਲਾਹੋ",
+ "multimediaviewer-view-expanded": "ਮੀਡੀਆ ਵਿਖਾਊ 'ਚ ਖੋਲ੍ਹੋ"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/pl.json b/www/wiki/extensions/MultimediaViewer/i18n/pl.json
new file mode 100644
index 00000000..ede7c224
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/pl.json
@@ -0,0 +1,110 @@
+{
+ "@metadata": {
+ "authors": [
+ "Chrumps",
+ "Jacenty359",
+ "Matik7",
+ "Nux",
+ "Peter Bowman",
+ "Tar Lócesilion",
+ "WTM",
+ "Blackfish",
+ "Matma Rex",
+ "Darellur",
+ "Macofe"
+ ]
+ },
+ "multimediaviewer-desc": "Powiększ miniatury ilustracji do wielkości ekranu.",
+ "multimediaviewer-pref": "Przeglądarka multimediów",
+ "multimediaviewer-pref-desc": "Popraw sposób oglądania ilustracji używając tego nowego narzędzia. Wyświetla ono ilustracje w większym rozmiarze bezpośrednio na stronach, które mają miniatury. Ilustracje te wyświetlane są w ładniejszym pełnoekranowym interfejsie, a nawet mogą być oglądane w pełnym rozmiarze.",
+ "multimediaviewer-optin-pref": "Aktywuj <span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About przeglądarkę multimediów]</span>",
+ "multimediaviewer-file-page": "Przejdź na stronę opisu pliku",
+ "multimediaviewer-repository-local": "Więcej szczegółów",
+ "multimediaviewer-datetime-created": "Utworzony: $1",
+ "multimediaviewer-datetime-uploaded": "Przesłany: $1",
+ "multimediaviewer-credit-fallback": "Pokaż informacje o autorze",
+ "multimediaviewer-multiple-authors": "{{PLURAL:$1|jeden inny autor|$1 innych autorów}}",
+ "multimediaviewer-multiple-authors-combine": "$1 i $2",
+ "multimediaviewer-metadata-error": "Nie można załadować szczegółów obrazu (błąd: $1)",
+ "multimediaviewer-thumbnail-error": "Niestety, nie można wyświetlić pliku",
+ "multimediaviewer-thumbnail-error-description": "Wygląda na to, że mamy problem techniczny. Możesz $1 lub $3, jeśli będzie nadal występował. Błąd: $2",
+ "multimediaviewer-thumbnail-error-retry": "ponowić próbę",
+ "multimediaviewer-thumbnail-error-report": "zgłosić problem",
+ "multimediaviewer-license-cc-pd": "Domena publiczna",
+ "multimediaviewer-license-pd": "Domena publiczna",
+ "multimediaviewer-license-default": "Zobacz licencję",
+ "multimediaviewer-permission-title": "Szczegóły pozwolenia",
+ "multimediaviewer-permission-link": "zobacz zasady",
+ "multimediaviewer-permission-viewmore": "Zobacz więcej",
+ "multimediaviewer-restriction-ihl": "Ten obrazek zawiera symbole zastrzeżone przez prawo konfliktów zbrojnych.",
+ "multimediaviewer-about-mmv": "O aplikacji",
+ "multimediaviewer-discuss-mmv": "Dyskusja",
+ "multimediaviewer-help-mmv": "Pomoc",
+ "multimediaviewer-optout-mmv": "Dezaktywuj przeglądarkę multimediów",
+ "multimediaviewer-optin-mmv": "Aktywuj przeglądarkę multimediów",
+ "multimediaviewer-optout-pending-mmv": "Dezaktywacja przeglądarki multimediów",
+ "multimediaviewer-optin-pending-mmv": "Aktywacja przeglądarki multimediów",
+ "multimediaviewer-optout-help": "Do przeglądania grafiki nie będzie już dłużej wykorzystywana przeglądarka multimedów. Aby użyć jej ponownie, kliknij na przycisk „{{int:multimediaviewer-view-expanded}}” w dowolnej grafice. Następnie kliknij na „{{int:multimediaviewer-optin-mmv}}”.",
+ "multimediaviewer-optin-help": "Przeglądarka multimediów będzie używana do wyświetlania grafik.",
+ "multimediaviewer-geolocation": "Położenie: $1",
+ "multimediaviewer-reuse-link": "Podziel się tym plikiem lub umieść go",
+ "multimediaviewer-reuse-loading-placeholder": "Wczytywanie…",
+ "multimediaviewer-share-tab": "Udostępnij",
+ "multimediaviewer-embed-tab": "Osadź",
+ "multimediaviewer-download-link": "Pobierz ten plik",
+ "multimediaviewer-download-preview-link-title": "Otwórz w przeglądarce",
+ "multimediaviewer-download-original-button-name": "Pobierz oryginalny plik",
+ "multimediaviewer-download-small-button-name": "Pobierz w małym rozmiarze",
+ "multimediaviewer-download-medium-button-name": "Pobierz w średnim rozmiarze",
+ "multimediaviewer-download-large-button-name": "Pobierz w dużym rozmiarze",
+ "multimediaviewer-link-to-page": "Link do strony opisu pliku",
+ "multimediaviewer-link-to-file": "Link do oryginalnego pliku",
+ "multimediaviewer-share-explanation": "Skopiuj i swobodnie dziel się linkiem",
+ "multimediaviewer-embed-wt": "Wikikod",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-embed-explanation": "Wykorzystaj ten kod do osadzenia pliku",
+ "multimediaviewer-html-embed-credit-link-text": "Link",
+ "multimediaviewer-embed-byline": "Autor: $1",
+ "multimediaviewer-embed-license": "Licencja: $1.",
+ "multimediaviewer-embed-via": "Na podstawie $1",
+ "multimediaviewer-default-embed-dimensions": "Domyślny rozmiar miniatury",
+ "multimediaviewer-original-embed-dimensions": "Oryginalny plik $1",
+ "multimediaviewer-large-embed-dimensions": "Duży rozmiar $1",
+ "multimediaviewer-medium-embed-dimensions": "Średni rozmiar $1",
+ "multimediaviewer-small-embed-dimensions": "Mały rozmiar $1",
+ "multimediaviewer-description-page-button-text": "Więcej szczegółów o pliku",
+ "multimediaviewer-description-page-popup-text": "Więcej szczegółów na temat tego pliku $1",
+ "multimediaviewer-commons-subtitle": "Repozytorium wolnych mediów",
+ "multimediaviewer-view-expanded": "Otwórz w Media Viewer",
+ "multimediaviewer-view-config": "Konfiguracja",
+ "multimediaviewer-close-popup-text": "Zamknij narzędzie (Esc)",
+ "multimediaviewer-fullscreen-popup-text": "Pokaż w trybie pełnoekranowym",
+ "multimediaviewer-defullscreen-popup-text": "Wyjdź z trybu pełnoekranowego",
+ "multimediaviewer-next-image-alt-text": "Pokaż następny obraz",
+ "multimediaviewer-prev-image-alt-text": "Pokaż poprzedni obraz",
+ "multimediaviewer-title-popup-text": "Opis",
+ "multimediaviewer-credit-popup-text": "Informacje o autorze i źródle",
+ "multimediaviewer-title-popup-text-more": "Zobacz pełny opis",
+ "multimediaviewer-credit-popup-text-more": "Pokaż pełne informacje o autorze i źródle",
+ "multimediaviewer-download-attribution-cta-header": "Powinieneś oznaczyć autora",
+ "multimediaviewer-download-optional-attribution-cta-header": "Możesz oznaczyć autora",
+ "multimediaviewer-download-attribution-cta": "Pokaż jak",
+ "multimediaviewer-attr-plain": "Zwykły tekst",
+ "multimediaviewer-options-tooltip": "Włącz albo wyłącz Media Viewer",
+ "multimediaviewer-options-dialog-header": "Wyłączyć Media Viewer?",
+ "multimediaviewer-options-text-header": "Pomiń wyświetlanie tej funkcji we wszystkich plikach.",
+ "multimediaviewer-options-text-body": "Możesz ją później wyłączyć na stronie ze szczegółami pliku.",
+ "multimediaviewer-options-learn-more": "Dowiedz się więcej",
+ "multimediaviewer-option-submit-button": "Wyłącz Media Viewer",
+ "multimediaviewer-option-cancel-button": "Anuluj",
+ "multimediaviewer-disable-confirmation-header": "Wyłączyłeś Media Viewer",
+ "multimediaviewer-disable-confirmation-text": "Kiedy następnym razem klikniesz na $1, od razu wyświetlą ci się wszystkie szczegóły pliku.",
+ "multimediaviewer-enable-dialog-header": "Włączyć Media Viewer?",
+ "multimediaviewer-enable-text-header": "Włącz to narzędzie wyświetlania multimediów domyślnie dla wszystkich plików.",
+ "multimediaviewer-enable-submit-button": "Włącz Media Viewer",
+ "multimediaviewer-enable-confirmation-header": "Włączyłeś Media Viewer dla wszystkich plików",
+ "multimediaviewer-enable-confirmation-text": "Kiedy następnym razem klikniesz na miniaturkę na $1, wyświetli się Media Viewer.",
+ "multimediaviewer-enable-alert": "Media Viewer jest wyłączony",
+ "multimediaviewer-disable-info-title": "Wyłączyłeś Media Viewer",
+ "multimediaviewer-disable-info": "Wciąż możesz oglądać pojedyncze pliki za pomocą Media Viewera"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/ps.json b/www/wiki/extensions/MultimediaViewer/i18n/ps.json
new file mode 100644
index 00000000..ec1cfee1
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/ps.json
@@ -0,0 +1,44 @@
+{
+ "@metadata": {
+ "authors": [
+ "Ahmed-Najib-Biabani-Ibrahimkhel"
+ ]
+ },
+ "multimediaviewer-pref": "رسنۍ ښکاره کوونکی",
+ "multimediaviewer-optin-pref": "<span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About رسنۍ کتونکی]</span> چارنول",
+ "multimediaviewer-repository-local": "نور تفصيل",
+ "multimediaviewer-datetime-created": "په $1 جوړ شو",
+ "multimediaviewer-datetime-uploaded": "په $1 پورته شو",
+ "multimediaviewer-credit-fallback": "د ليکوال مالومات کتل",
+ "multimediaviewer-license-cc-pd": "ټولگړی شپول",
+ "multimediaviewer-license-default": "د منښتليک مالومات",
+ "multimediaviewer-permission-title": "د اجازې تفصيل",
+ "multimediaviewer-about-mmv": "په اړه",
+ "multimediaviewer-discuss-mmv": "خبرې اترې",
+ "multimediaviewer-optout-mmv": "رسنۍ کتونکی ناچارنول",
+ "multimediaviewer-optin-mmv": "رسنۍ کتونکی چارنول",
+ "multimediaviewer-optout-pending-mmv": "رسنۍ کتونکی ناچارنول",
+ "multimediaviewer-reuse-link": "دا دوتنه کارول",
+ "multimediaviewer-download-link": "همدا دوتنه ښکته کول",
+ "multimediaviewer-download-preview-link-title": "په کتنمل کې کتل",
+ "multimediaviewer-download-original-button-name": "اصلي دوتنه ښکته کول",
+ "multimediaviewer-link-to-file": "اصلي دوتنې ته تړنه",
+ "multimediaviewer-original-embed-dimensions": "اصلي دوتنه $1",
+ "multimediaviewer-view-expanded": "په رسنۍ کتونکي کې پرانيستل",
+ "multimediaviewer-close-popup-text": "دا اوزار تړل (Esc)",
+ "multimediaviewer-fullscreen-popup-text": "په ډکه پرده کې ښکاره کول",
+ "multimediaviewer-download-optional-attribution-cta-header": "ليکوال ته يې اړونده کولی شی",
+ "multimediaviewer-options-tooltip": "رسنۍ کتونکی چارنول يا ناچارنول",
+ "multimediaviewer-options-dialog-header": "آيا رسنۍ کتونکی ناچارنوې؟",
+ "multimediaviewer-options-text-header": "د ټولو دوتنو لپاره د کتلو همدغو ځانتياوو څخه تېرېدل.",
+ "multimediaviewer-options-text-body": "تاسې يې د مخ تفصيلونو له مخې وروسته چارنولی شئ.",
+ "multimediaviewer-option-submit-button": "رسنۍ کتونکی ناچارنول",
+ "multimediaviewer-disable-confirmation-text": "بل ځل ته چې تاسې د $1 په يو بټنوک ټک ورکوئ، نو سمدلاسه به د دوتنې ټول تفصيل درښکاره شي.",
+ "multimediaviewer-enable-dialog-header": "رسنۍ کتونکی چارنوې؟",
+ "multimediaviewer-enable-text-header": "د همدې رسنۍ د کتلو ځانتياوې په تلواليزه توگه د ټولو دوتنو لپاره چارنول.",
+ "multimediaviewer-enable-submit-button": "رسنۍ کتونکی چارنول",
+ "multimediaviewer-enable-confirmation-header": "تاسې د ټولو دوتنو لپاره رسنۍ کتونکی چارن کړی",
+ "multimediaviewer-enable-confirmation-text": "بل ځل ته چې تاسې د $1 په يو بټنوک ټک ورکوئ، نو رسنۍ کتونکی به وکارېږي.",
+ "multimediaviewer-disable-info-title": "تاسې رسنۍ کتونکی ناچارن کړ",
+ "multimediaviewer-disable-info": "تاسې لا تر اوسه پورې د رسنۍ کتونکي په مرسته ځانگړې دوتنې کتلی شی."
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/pt-br.json b/www/wiki/extensions/MultimediaViewer/i18n/pt-br.json
new file mode 100644
index 00000000..3f768ac8
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/pt-br.json
@@ -0,0 +1,161 @@
+{
+ "@metadata": {
+ "authors": [
+ "Oona",
+ "Jefersonmoraes",
+ "Ptrke",
+ "TheEduGobi",
+ "Araceletorres",
+ "Macofe",
+ "Eduardo Addad de Oliveira",
+ "Felipe L. Ewald"
+ ]
+ },
+ "multimediaviewer-desc": "Expandir miniaturas em tamanho maior numa interface em tela cheia.",
+ "multimediaviewer-pref": "Visualizador multimídia",
+ "multimediaviewer-pref-desc": "Melhore a sua experiência de visualização multimídia com esta nova ferramenta. Ela exibe imagens em tamanho maior nas páginas que possuam miniaturas. As imagens são exibidas em uma agradável sobreposição em tela cheia, e também podem ser visualizadas em tamanho real.",
+ "multimediaviewer-optin-pref": "Ativar <span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About Media Viewer]</span>",
+ "multimediaviewer-file-page": "Ir para a página do arquivo correspondente",
+ "multimediaviewer-repository-local": "Mais detalhes",
+ "multimediaviewer-datetime-created": "Criada: $1",
+ "multimediaviewer-datetime-uploaded": "Enviado: $1",
+ "multimediaviewer-credit": "$1 - $2",
+ "multimediaviewer-credit-fallback": "Ver informações de autor",
+ "multimediaviewer-multiple-authors": "{{PLURAL:$1|mais um autor|mais $1 autores}}",
+ "multimediaviewer-multiple-authors-combine": "$1 e $2",
+ "multimediaviewer-metadata-error": "Não foi possível carregar os detalhes da imagem (error:$1)",
+ "multimediaviewer-thumbnail-error": "Desculpe, o arquivo não pode ser exibido",
+ "multimediaviewer-thumbnail-error-description": "Parece haver um problema técnico. Você pode $1 ou $3, caso o problema continue. Erro: $2",
+ "multimediaviewer-thumbnail-error-retry": "tentar novamente",
+ "multimediaviewer-thumbnail-error-report": "comunicar o erro",
+ "multimediaviewer-license-cc-by-1.0": "CC BY 1.0",
+ "multimediaviewer-license-cc-sa-1.0": "CC SA 1.0",
+ "multimediaviewer-license-cc-by-sa-1.0": "CC BY-SA 1.0",
+ "multimediaviewer-license-cc-by-2.0": "CC BY 2.0",
+ "multimediaviewer-license-cc-by-sa-2.0": "CC BY-SA 2.0",
+ "multimediaviewer-license-cc-by-2.1": "CC BY 2.1",
+ "multimediaviewer-license-cc-by-sa-2.1": "CC BY-SA 2.1",
+ "multimediaviewer-license-cc-by-2.5": "CC BY 2.5",
+ "multimediaviewer-license-cc-by-sa-2.5": "CC BY-SA 2.5",
+ "multimediaviewer-license-cc-by-3.0": "CC BY 3.0",
+ "multimediaviewer-license-cc-by-sa-3.0": "CC BY-SA 3.0",
+ "multimediaviewer-license-cc-by-4.0": "CC BY 4.0",
+ "multimediaviewer-license-cc-by-sa-4.0": "CC BY-SA 4.0",
+ "multimediaviewer-license-cc-pd": "Domínio Público",
+ "multimediaviewer-license-cc-zero": "CC 0",
+ "multimediaviewer-license-pd": "Domínio Público",
+ "multimediaviewer-license-default": "Ver licença",
+ "multimediaviewer-permission-title": "Detalhes da permissão",
+ "multimediaviewer-permission-link": "ver termos",
+ "multimediaviewer-permission-link-hide": "ocultar termos",
+ "multimediaviewer-permission-viewmore": "Ver mais",
+ "multimediaviewer-restriction-2257": "Esta imagem contém conteúdo sexualmente explícito que pode estar sujeito à Lei de Proteção à Criança e Proteção de Obscenidade nos Estados Unidos.",
+ "multimediaviewer-restriction-aus-reserve": "Esta imagem foi fotografada em uma reserva da Commonwealth australiana e não pode ser usada para ganhos comerciais sem autorização.",
+ "multimediaviewer-restriction-communist": "Esta imagem contém insígnias comunistas que podem ser banidas em alguns países.",
+ "multimediaviewer-restriction-costume": "Esta imagem representa fantasias e pode estar sujeita a restrições legais.",
+ "multimediaviewer-restriction-currency": "Esta imagem representa uma representação de uma unidade de moeda e pode estar sujeita a restrições legais.",
+ "multimediaviewer-restriction-design": "O design do assunto desta imagem pode ser protegido por direitos autorais e sujeito a restrições legais.",
+ "multimediaviewer-restriction-fan-art": "Esta imagem é um trabalho de arte de fãs e a reutilização pode estar sujeita a restrições legais.",
+ "multimediaviewer-restriction-ihl": "Esta imagem contém símbolos restritos pelo Direito Internacional Humanitário.",
+ "multimediaviewer-restriction-insignia": "Esta imagem contém insígnias oficiais que podem estar sujeitas a restrições legais.",
+ "multimediaviewer-restriction-ita-mibac": "Esta imagem reproduz uma propriedade pertencente ao patrimônio cultural italiano e é restringida pela lei italiana.",
+ "multimediaviewer-restriction-nazi": "Esta imagem contém nazistas ou outras insígnias fascistas que podem ser banidas em alguns países.",
+ "multimediaviewer-restriction-personality": "Esta imagem contém pessoas que podem ter direitos que restringem legalmente certas reutilizações da imagem sem consentimento.",
+ "multimediaviewer-restriction-trademarked": "Esta imagem contém conteúdo que pode estar sujeito a leis de marca registada.",
+ "multimediaviewer-restriction-default": "Esta imagem pode ser restrita por disposições legais fora da lei de direitos autorais. Veja a página de descrição do arquivo para obter detalhes.",
+ "multimediaviewer-restriction-default-and-others": "Esta imagem pode ser restringida por outras disposições legais fora da lei de direitos autorais. Veja a página de descrição do arquivo para obter detalhes.",
+ "multimediaviewer-about-mmv": "Sobre",
+ "multimediaviewer-discuss-mmv": "Discussão",
+ "multimediaviewer-help-mmv": "Ajuda",
+ "multimediaviewer-optout-mmv": "Desativar visualizador multimídia",
+ "multimediaviewer-optin-mmv": "Ativar visualizador multimídia",
+ "multimediaviewer-optout-pending-mmv": "Desativar visualizador multimídia",
+ "multimediaviewer-optin-pending-mmv": "Ativar visualizador multimídia",
+ "multimediaviewer-optout-help": "O visualizador multimídia não será mais utilizado para exibir imagens. Para ativá-lo novamente, clique no botão \"{{int:multimediaviewer-view-expanded}}\" próximo a qualquer imagem. Depois, clique em \"{{int:multimediaviewer-optin-mmv}}\".",
+ "multimediaviewer-optin-help": "O visualizador multimídia será utilizado para exibir imagens.",
+ "multimediaviewer-geoloc-north": "N",
+ "multimediaviewer-geoloc-east": "E",
+ "multimediaviewer-geoloc-south": "S",
+ "multimediaviewer-geoloc-west": "W",
+ "multimediaviewer-geoloc-coord": "$1 $2 $3 $4",
+ "multimediaviewer-geoloc-coords": "$1, $2",
+ "multimediaviewer-geolocation": "Localização: $1",
+ "multimediaviewer-reuse-link": "Compartilhar ou anexar este arquivo",
+ "multimediaviewer-reuse-loading-placeholder": "Carregando…",
+ "multimediaviewer-reuse-copy-share": "Seleciona e copia (se suportado) o link para compartilhar este arquivo",
+ "multimediaviewer-reuse-copy-embed": "Seleciona e copia (se suportado) o código para incorporar este arquivo",
+ "multimediaviewer-share-tab": "Compartilhar",
+ "multimediaviewer-embed-tab": "Incorporar",
+ "multimediaviewer-download-link": "Baixar este arquivo",
+ "multimediaviewer-download-preview-link-title": "Ver no navegador",
+ "multimediaviewer-download-original-button-name": "Baixar arquivo original",
+ "multimediaviewer-download-small-button-name": "Baixar em tamanho pequeno",
+ "multimediaviewer-download-medium-button-name": "Baixar em tamanho médio",
+ "multimediaviewer-download-large-button-name": "Baixar em tamanho grande",
+ "multimediaviewer-link-to-page": "Link para a página de descrição do arquivo",
+ "multimediaviewer-link-to-file": "Link para o arquivo original",
+ "multimediaviewer-share-explanation": "Copiar e compartilhar o link",
+ "multimediaviewer-embed-wt": "Wikitexto",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-embed-explanation": "Usar este código para incorporar o arquivo",
+ "multimediaviewer-text-embed-credit-text-bl": "Por $1, $2, $3",
+ "multimediaviewer-text-embed-credit-text-b": "Por $1, $2",
+ "multimediaviewer-text-embed-credit-text-l": "$1, $2",
+ "multimediaviewer-html-embed-credit-text-bl": "Por $1, $2, $3",
+ "multimediaviewer-html-embed-credit-text-b": "Por $1, $2",
+ "multimediaviewer-html-embed-credit-text-l": "$1, $2",
+ "multimediaviewer-html-embed-credit-link-text": "Link",
+ "multimediaviewer-embed-byline": "Por $1",
+ "multimediaviewer-embed-license": "Licenciado sob $1.",
+ "multimediaviewer-embed-license-nonfree": "$1.",
+ "multimediaviewer-embed-via": "Via $1.",
+ "multimediaviewer-default-embed-dimensions": "Tamanho de miniatura padrão",
+ "multimediaviewer-original-embed-dimensions": "Arquivo original $1",
+ "multimediaviewer-large-embed-dimensions": "Grande $1",
+ "multimediaviewer-medium-embed-dimensions": "Médio $1",
+ "multimediaviewer-small-embed-dimensions": "Pequeno $1",
+ "multimediaviewer-embed-dimensions": "$1 × $2",
+ "multimediaviewer-embed-dimensions-separated": "- $1",
+ "multimediaviewer-description-page-button-text": "Mais detalhes sobre este arquivo",
+ "multimediaviewer-description-page-popup-text": "Mais detalhes sobre este arquivo em $1",
+ "multimediaviewer-commons-subtitle": "O repositório de mídias livres",
+ "multimediaviewer-view-expanded": "Abrir no Visualizador de Mídia",
+ "multimediaviewer-view-config": "Configuração",
+ "multimediaviewer-close-popup-text": "Fechar esta ferramenta (Esc)",
+ "multimediaviewer-fullscreen-popup-text": "Mostrar em tela inteira",
+ "multimediaviewer-defullscreen-popup-text": "Sair do modo tela inteira",
+ "multimediaviewer-next-image-alt-text": "Mostrar próxima imagem",
+ "multimediaviewer-prev-image-alt-text": "Mostrar imagem anterior",
+ "multimediaviewer-title-popup-text": "Descrição",
+ "multimediaviewer-credit-popup-text": "Autor e informação sobre a fonte",
+ "multimediaviewer-title-popup-text-more": "Ver descrição completa",
+ "multimediaviewer-credit-popup-text-more": "Ver nome completo do autor e fonte",
+ "multimediaviewer-download-attribution-cta-header": "Você precisa indicar o autor",
+ "multimediaviewer-download-optional-attribution-cta-header": "Você pode indicar o autor",
+ "multimediaviewer-download-attribution-cta": "Mostre-me como",
+ "multimediaviewer-download-attribution-copy": "Seleciona e copia (se suportado) o texto de atribuição para este arquivo",
+ "multimediaviewer-reuse-warning-deletion": "Este arquivo é considerado para exclusão.",
+ "multimediaviewer-reuse-warning-nonfree": "Este arquivo não possui uma licença gratuita.",
+ "multimediaviewer-reuse-warning-noattribution": "Este arquivo não possui informações de atribuição.",
+ "multimediaviewer-reuse-warning-generic": "Verifique [$1 os detalhes] antes de utilizá-lo.",
+ "multimediaviewer-attr-plain": "Simples",
+ "multimediaviewer-attr-html": "HTML",
+ "multimediaviewer-options-tooltip": "Ativar ou desativar visualizador multimídia",
+ "multimediaviewer-options-dialog-header": "Desativar visualizador multimídia?",
+ "multimediaviewer-options-text-header": "Ignorar este recurso de visualização para todos os arquivos.",
+ "multimediaviewer-options-text-body": "Você pode reativá-lo mais tarde através da página de detalhes dos arquivos.",
+ "multimediaviewer-options-learn-more": "Saiba mais",
+ "multimediaviewer-option-submit-button": "Desativar visualizador multimídia",
+ "multimediaviewer-option-cancel-button": "Cancelar",
+ "multimediaviewer-disable-confirmation-header": "Você desativou o visualizador multimídia",
+ "multimediaviewer-disable-confirmation-text": "Da próxima vez que você clicar numa miniatura em $1, terá acesso direto aos detalhes do arquivo.",
+ "multimediaviewer-enable-dialog-header": "Ativar visualizador multimídia?",
+ "multimediaviewer-enable-text-header": "Ativar este recurso de visualização multimídia para todos os arquivos por padrão.",
+ "multimediaviewer-enable-submit-button": "Ativar visualizador multimídia",
+ "multimediaviewer-enable-confirmation-header": "Você ativou o visualizador multimídia para todos os arquivos",
+ "multimediaviewer-enable-confirmation-text": "Da próxima vez que você clicar numa miniatura em $1, o visualizador multimídia será usado.",
+ "multimediaviewer-enable-alert": "O visualizador multimídia agora está desativado",
+ "multimediaviewer-disable-info-title": "Você desativou o visualizador multimídia",
+ "multimediaviewer-disable-info": "Você pode ainda ver os arquivos individuais com o visualizador multimídia.",
+ "multimediaviewer-errorreport-privacywarning": "Os detalhes do erro são anexados ao relatório, que será visível publicamente. Se você não está confortável com isso, você pode editar o relatório abaixo e remover todos os dados que você não deseja compartilhar."
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/pt.json b/www/wiki/extensions/MultimediaViewer/i18n/pt.json
new file mode 100644
index 00000000..0b0d1f40
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/pt.json
@@ -0,0 +1,132 @@
+{
+ "@metadata": {
+ "authors": [
+ "Fúlvio",
+ "Imperadeiro98",
+ "SandroHc",
+ "Vitorvicentevalente",
+ "Macofe",
+ "Hamilton Abreu"
+ ]
+ },
+ "multimediaviewer-desc": "Ampliar miniaturas para tamanho maior em ecrã cheio.",
+ "multimediaviewer-pref": "Visualizador Multimédia",
+ "multimediaviewer-pref-desc": "Melhore a sua experiência de visionamento multimédia com esta ferramenta nova. Ela apresenta imagens em tamanho maior nas páginas que tenham miniaturas. As imagens são mostradas numa agradável sobreposição em ecrã cheio e também podem ser vistas em tamanho real.",
+ "multimediaviewer-optin-pref": "Ativar <span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About Visualizador Multimédia]</span>",
+ "multimediaviewer-file-page": "Ir para a página de ficheiro correspondente",
+ "multimediaviewer-repository-local": "Mais detalhes",
+ "multimediaviewer-datetime-created": "Criação: $1",
+ "multimediaviewer-datetime-uploaded": "Carregamento: $1",
+ "multimediaviewer-credit-fallback": "Ver informação do autor",
+ "multimediaviewer-multiple-authors": "{{PLURAL:$1|mais um autor|mais $1 autores}}",
+ "multimediaviewer-multiple-authors-combine": "$1 e $2",
+ "multimediaviewer-metadata-error": "Não foi possível carregar os dados da imagem (erro: $1)",
+ "multimediaviewer-thumbnail-error": "Desculpe, o ficheiro não pode ser apresentado",
+ "multimediaviewer-thumbnail-error-description": "Parece haver um problema técnico. Pode $1 ou $3, caso o problema continue. Erro: $2",
+ "multimediaviewer-thumbnail-error-retry": "tentar novamente",
+ "multimediaviewer-thumbnail-error-report": "comunicar o erro",
+ "multimediaviewer-license-cc-pd": "Domínio Público",
+ "multimediaviewer-license-pd": "Domínio público",
+ "multimediaviewer-license-default": "Ver licença",
+ "multimediaviewer-permission-title": "Detalhes da permissão",
+ "multimediaviewer-permission-link": "ver termos",
+ "multimediaviewer-permission-link-hide": "ocultar termos",
+ "multimediaviewer-permission-viewmore": "Ver mais",
+ "multimediaviewer-restriction-2257": "Esta imagem contém conteúdo sexualmente explícito que pode estar sujeito ao Child Protection and Obscenity Enforcement Act nos Estados Unidos.",
+ "multimediaviewer-restriction-aus-reserve": "Esta imagem foi fotografada numa reserva da Comunidade Australiana e não pode ser usada para fins comerciais sem autorização.",
+ "multimediaviewer-restriction-communist": "Esta imagem contém uma insígnia comunista que pode ser proibida em alguns países.",
+ "multimediaviewer-restriction-costume": "Esta imagem mostra objetos de vestuário e pode estar sujeita a restrições legais.",
+ "multimediaviewer-restriction-currency": "Esta imagem representa uma unidade monetária e pode estar sujeita a restrições legais.",
+ "multimediaviewer-restriction-design": "A conceção do motivo desta imagem pode estar protegida por direitos de autor e sujeita a restrições legais.",
+ "multimediaviewer-restriction-fan-art": "Esta imagem é um trabalho de arte de fãs (''fan art''), e pode estar sujeita a restrições legais.",
+ "multimediaviewer-restriction-ihl": "Esta imagem contém símbolos restringidos pelo Direito Humanitário Internacional",
+ "multimediaviewer-restriction-insignia": "Esta imagem contém uma insígnia oficial que pode estar sujeita a restrições legais.",
+ "multimediaviewer-restriction-ita-mibac": "Esta imagem reproduz uma propriedade que pertence à herança cultural italiana e está restringida pela lei italiana.",
+ "multimediaviewer-restriction-nazi": "Esta imagem contém uma insígnia nazista ou outra considerada fascista que pode ser proibida em alguns países.",
+ "multimediaviewer-restriction-personality": "Esta imagem contém pessoas que podem deter direitos de restrição legal de certas reutilizações da imagem sem autorização.",
+ "multimediaviewer-restriction-trademarked": "Esta imagem contém conteúdos que podem estar sujeitos a legislação sobre marcas registadas.",
+ "multimediaviewer-restriction-default": "O uso desta imagem pode estar restringido por disposições legais fora do âmbito da legislação de direitos de autor. Veja a página de descrição do ficheiro para detalhes.",
+ "multimediaviewer-restriction-default-and-others": "O uso desta imagem pode estar restringido por disposições legais fora do âmbito da legislação de direitos de autor. Veja a página de descrição do ficheiro para detalhes.",
+ "multimediaviewer-about-mmv": "Sobre",
+ "multimediaviewer-discuss-mmv": "Discussão",
+ "multimediaviewer-help-mmv": "Ajuda",
+ "multimediaviewer-optout-mmv": "Desativar Visualizador Multimédia",
+ "multimediaviewer-optin-mmv": "Ativar Visualizador Multimédia",
+ "multimediaviewer-optout-pending-mmv": "A desativar o Visualizador Multimédia",
+ "multimediaviewer-optin-pending-mmv": "A ativar o Visualizador Multimédia",
+ "multimediaviewer-optout-help": "O Visualizador Multimédia deixará de ser utilizado para mostrar imagens. Para o ativar novamente, clique o botão \"{{int:multimediaviewer-view-expanded}}\" ao lado de qualquer imagem. Depois, clique \"{{int:multimediaviewer-optin-mmv}}\".",
+ "multimediaviewer-optin-help": "O Visualizador Multimédia será utilizado para mostrar imagens.",
+ "multimediaviewer-geolocation": "Localização: $1",
+ "multimediaviewer-reuse-link": "Partilhar ou incorporar este ficheiro",
+ "multimediaviewer-reuse-loading-placeholder": "A carregar...",
+ "multimediaviewer-reuse-copy-share": "Selecionar e copiar (se suportado) a hiperligação para partilhar este ficheiro",
+ "multimediaviewer-reuse-copy-embed": "Seleciona e copia (se suportado) o código para incorporar este ficheiro",
+ "multimediaviewer-share-tab": "Partilhar",
+ "multimediaviewer-embed-tab": "Incorporar",
+ "multimediaviewer-download-link": "Descarregar este ficheiro",
+ "multimediaviewer-download-preview-link-title": "Visualizar no navegador",
+ "multimediaviewer-download-original-button-name": "Descarregar em tamanho original",
+ "multimediaviewer-download-small-button-name": "Descarregar em tamanho pequeno",
+ "multimediaviewer-download-medium-button-name": "Descarregar em tamanho médio",
+ "multimediaviewer-download-large-button-name": "Descarregar em tamanho grande",
+ "multimediaviewer-link-to-page": "Hiperligação para a página de descrição do ficheiro",
+ "multimediaviewer-link-to-file": "Hiperligação para o ficheiro original",
+ "multimediaviewer-share-explanation": "Copiar e partilhar livremente a hiperligação",
+ "multimediaviewer-embed-wt": "Texto wiki",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-embed-explanation": "Usar este código para incorporar o ficheiro",
+ "multimediaviewer-text-embed-credit-text-bl": "Por $1, $2, $3",
+ "multimediaviewer-text-embed-credit-text-b": "Por $1, $2",
+ "multimediaviewer-html-embed-credit-text-bl": "Por $1, $2, $3",
+ "multimediaviewer-html-embed-credit-text-b": "Por $1, $2",
+ "multimediaviewer-html-embed-credit-link-text": "Hiperligação",
+ "multimediaviewer-embed-byline": "Por $1",
+ "multimediaviewer-embed-license": "Licenciado ao abrigo da licença $1.",
+ "multimediaviewer-embed-via": "Via $1.",
+ "multimediaviewer-default-embed-dimensions": "Tamanho de miniatura padrão",
+ "multimediaviewer-original-embed-dimensions": "Tamanho original $1",
+ "multimediaviewer-large-embed-dimensions": "Grande $1",
+ "multimediaviewer-medium-embed-dimensions": "Médio $1",
+ "multimediaviewer-small-embed-dimensions": "Pequeno $1",
+ "multimediaviewer-description-page-button-text": "Mais detalhes sobre este ficheiro",
+ "multimediaviewer-description-page-popup-text": "Mais detalhes sobre este ficheiro em $1",
+ "multimediaviewer-commons-subtitle": "O repositório de multimédia livre",
+ "multimediaviewer-view-expanded": "Abrir no Visualizador Multimédia",
+ "multimediaviewer-view-config": "Configuração",
+ "multimediaviewer-close-popup-text": "Fechar esta ferramenta (Esc)",
+ "multimediaviewer-fullscreen-popup-text": "Mostrar em ecrã inteiro",
+ "multimediaviewer-defullscreen-popup-text": "Sair do ecrã inteiro",
+ "multimediaviewer-next-image-alt-text": "Mostrar a próxima imagem",
+ "multimediaviewer-prev-image-alt-text": "Mostrar a imagem anterior",
+ "multimediaviewer-title-popup-text": "Descrição",
+ "multimediaviewer-credit-popup-text": "Autor e informação sobre a fonte",
+ "multimediaviewer-title-popup-text-more": "Ver descrição completa",
+ "multimediaviewer-credit-popup-text-more": "Ver nome completo do autor e fonte",
+ "multimediaviewer-download-attribution-cta-header": "Precisa de indicar o autor",
+ "multimediaviewer-download-optional-attribution-cta-header": "Pode indicar o autor",
+ "multimediaviewer-download-attribution-cta": "Mostre-me como",
+ "multimediaviewer-download-attribution-copy": "Seleciona e copia (se suportado) o texto de atribuição deste ficheiro",
+ "multimediaviewer-reuse-warning-deletion": "Este ficheiro está a ser considerado para eliminação.",
+ "multimediaviewer-reuse-warning-nonfree": "Este ficheiro não tem uma licença livre.",
+ "multimediaviewer-reuse-warning-noattribution": "Este ficheiro não tem informação de atribuição do crédito.",
+ "multimediaviewer-reuse-warning-generic": "Verifique [$1 os detalhes] antes de utilizá-lo.",
+ "multimediaviewer-attr-plain": "Simples",
+ "multimediaviewer-options-tooltip": "Ativar ou desativar Visualizador Multimédia",
+ "multimediaviewer-options-dialog-header": "Desativar o Visualizador Multimédia?",
+ "multimediaviewer-options-text-header": "Ignorar esta funcionalidade para todos os ficheiros.",
+ "multimediaviewer-options-text-body": "Pode reativá-la mais tarde através das páginas dos ficheiros.",
+ "multimediaviewer-options-learn-more": "Saiba mais",
+ "multimediaviewer-option-submit-button": "Desativar Visualizador Multimédia",
+ "multimediaviewer-option-cancel-button": "Cancelar",
+ "multimediaviewer-disable-confirmation-header": "Desativou o Visualizador Multimédia",
+ "multimediaviewer-disable-confirmation-text": "Da próxima vez que clicar numa miniatura em $1, terá acesso direto aos detalhes do ficheiro.",
+ "multimediaviewer-enable-dialog-header": "Ativar o Visualizador Multimédia?",
+ "multimediaviewer-enable-text-header": "Ativar esta funcionalidade de visualização multimédia para todos os ficheiros por padrão.",
+ "multimediaviewer-enable-submit-button": "Ativar o Visualizador Multimédia",
+ "multimediaviewer-enable-confirmation-header": "Ativou o Visualizador Multimédia para todos os ficheiros",
+ "multimediaviewer-enable-confirmation-text": "Da próxima vez que clicar uma miniatura em $1, o Visualizador Multimédia será usado.",
+ "multimediaviewer-enable-alert": "O Visualizador Multimédia está agora desativado",
+ "multimediaviewer-disable-info-title": "Desativou o Visualizador Multimédia",
+ "multimediaviewer-disable-info": "Pode ainda ver ficheiros individuais com o Visualizador Multimédia.",
+ "multimediaviewer-errorreport-privacywarning": "Os detalhes do erro foram anexados ao relatório, que será publicamente visível. Se não concordar, pode editar o relatório abaixo e remover todos os dados que não deseja partilhar."
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/qqq.json b/www/wiki/extensions/MultimediaViewer/i18n/qqq.json
new file mode 100644
index 00000000..0071595c
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/qqq.json
@@ -0,0 +1,166 @@
+{
+ "@metadata": {
+ "authors": [
+ "Mark Holmquist <mtraceur@member.fsf.org>",
+ "Raymond",
+ "Shirayuki",
+ "Liuxinyu970226",
+ "Robby",
+ "Mormegil",
+ "Amire80",
+ "Tacsipacsi",
+ "Wladek92",
+ "Pikne",
+ "Jon Harald Søby"
+ ]
+ },
+ "multimediaviewer-desc": "{{desc|name=Multimedia Viewer|url=https://www.mediawiki.org/wiki/Extension:MultimediaViewer}}",
+ "multimediaviewer-pref": "Preference title",
+ "multimediaviewer-pref-desc": "Description of preference",
+ "multimediaviewer-optin-pref": "Label for non-beta preference.",
+ "multimediaviewer-file-page": "Text for a link to the file page for an image.",
+ "multimediaviewer-repository-local": "Link to repository where the image is locally hosted.\nSee also:\n* {{msg-mw|multimediaviewer-repository}}",
+ "multimediaviewer-datetime-created": "Used in JavaScript code. Parameters:\n* $1 - time and date\nSee also:\n* {{msg-mw|Multimediaviewer-datetime-uploaded}}\n\nNote that the date comes from the description page and can have various forms (e.g. \"2000-01-01\", \"1st January 2000\", \"1492\", \"16th century\"...)\n\nAlso, this might refer to the date when the picture was taken, or the date when the depcited work was made - the usage is not consistent. You should choose a translation that works in all cases.\n{{Identical|Created}}",
+ "multimediaviewer-datetime-uploaded": "Used in JavaScript code. Parameters:\n* $1 - time and date (formatted)\nSee also:\n* {{msg-mw|Multimediaviewer-datetime-created}}\n{{Identical|Uploaded}}",
+ "multimediaviewer-credit": "Credit line for images. Parameters:\n* $1 - HTML describing the author\n* $2 - HTML describing the source\n\nNeither parameters are usernames, so GENDER is useless. Both come directly from the API, the extended metadata imageinfo prop in particular.\n\nThey will usually be derived from the HTML output from wikitext on a file description page - however, no complicated HTML, only links, will be allowed.\n\nSee also {{msg-mw|multimediaviewer-credit-fallback}}",
+ "multimediaviewer-credit-fallback": "Text shown in place of the credit line ({{msg-mw|multimediaviewer-credit}}) when neither author nor source information is available.",
+ "multimediaviewer-multiple-authors": "Text shown after the author name when there are multiple authors. The text will link to the file description page.\n* $1 - number of additional authors.",
+ "multimediaviewer-multiple-authors-combine": "Combines the author name and the message about other authors.\n* $1 - author name, parsed from the file page\n* $2 - {{msg-mw|multimediaviewer-multiple-authors}} wrapped in a link.\n{{Identical|And}}",
+ "multimediaviewer-metadata-error": "Text shown when the information on the metadata panel could not be loaded. Parameters:\n* $1 - the error message (not localized)\nSee also:\n* {{msg-mw|multimediaviewer-thumbnail-error}}",
+ "multimediaviewer-thumbnail-error": "Text shown when the image could not be loaded. Followed by {{msg-mw|multimediaviewer-thumbnail-error-description}}.\nSee also:\n* {{msg-mw|multimediaviewer-thumbnail-error-description}}\n* {{msg-mw|multimediaviewer-metadata-error}}",
+ "multimediaviewer-thumbnail-error-description": "Text shown when the image could not be loaded. Follows {{msg-mw|multimediaviewer-thumbnail-error}}. Parameters:\n* $1 - \"retry\" link (see {{msg-mw|multimediaviewer-thumbnail-error-retry}})\n* $2 - the error message (not localized)\n* $3 - report link (creates a Phabricator task; see {{msg-mw|multimediaviewer-thumbnail-error-report}})\nSee also:\n* {{msg-mw|multimediaviewer-thumbnail-error}}",
+ "multimediaviewer-thumbnail-error-retry": "Used as a part of {{msg-mw|multimediaviewer-thumbnail-error-description}} (as a link text).\n{{Identical|Retry}}",
+ "multimediaviewer-thumbnail-error-report": "Used as a part of {{msg-mw|multimediaviewer-thumbnail-error-description}} (as a link text).",
+ "multimediaviewer-report-issue-url": "{{ignored}}URL to the report issue website",
+ "multimediaviewer-license-cc-by-1.0": "Very short label for the Creative Commons Attribution license, version 1.0, used in a link to the file information page that has more licensing information.\n{{Identical|CC BY}}",
+ "multimediaviewer-license-cc-sa-1.0": "Very short label for the Creative Commons ShareAlike license, version 1.0, used in a link to the file information page that has more licensing information.",
+ "multimediaviewer-license-cc-by-sa-1.0": "Very short label for the Creative Commons Attribution ShareAlike license, version 1.0, used in a link to the file information page that has more licensing information.\n{{Identical|CC BY-SA}}",
+ "multimediaviewer-license-cc-by-2.0": "Very short label for the Creative Commons Attribution license, version 2.0, used in a link to the file information page that has more licensing information.\n{{Identical|CC BY}}",
+ "multimediaviewer-license-cc-by-sa-2.0": "Very short label for the Creative Commons Attribution ShareAlike license, version 2.0, used in a link to the file information page that has more licensing information.\n{{Identical|CC BY-SA}}",
+ "multimediaviewer-license-cc-by-2.1": "Very short label for the Creative Commons Attribution license, version 2.1, used in a link to the file information page that has more licensing information.\n{{Identical|CC BY}}",
+ "multimediaviewer-license-cc-by-sa-2.1": "Very short label for the Creative Commons Attribution ShareAlike license, version 2.1, used in a link to the file information page that has more licensing information.\n{{Identical|CC BY-SA}}",
+ "multimediaviewer-license-cc-by-2.5": "Very short label for the Creative Commons Attribution license, version 2.5, used in a link to the file information page that has more licensing information.\n{{Identical|CC BY}}",
+ "multimediaviewer-license-cc-by-sa-2.5": "Very short label for the Creative Commons Attribution ShareAlike license, version 2.5, used in a link to the file information page that has more licensing information.\n{{Identical|CC BY-SA}}",
+ "multimediaviewer-license-cc-by-3.0": "Very short label for the Creative Commons Attribution license, version 3.0, used in a link to the file information page that has more licensing information.\n{{Identical|CC BY}}",
+ "multimediaviewer-license-cc-by-sa-3.0": "Very short label for the Creative Commons Attribution ShareAlike license, version 3.0, used in a link to the file information page that has more licensing information.\n{{Identical|CC BY-SA}}",
+ "multimediaviewer-license-cc-by-4.0": "Very short label for the Creative Commons Attribution license, version 4.0, used in a link to the file information page that has more licensing information.\n{{Identical|CC BY}}",
+ "multimediaviewer-license-cc-by-sa-4.0": "Very short label for the Creative Commons Attribution ShareAlike license, version 4.0, used in a link to the file information page that has more licensing information.\n{{Identical|CC BY-SA}}",
+ "multimediaviewer-license-cc-pd": "Very short label for the Creative Commons Public Domain license, used in a link to the file information page that has more licensing information.\n{{Identical|Public domain}}",
+ "multimediaviewer-license-cc-zero": "Very short label for the Creative Commons Zero license, used in a link to the file information page that has more licensing information.",
+ "multimediaviewer-license-pd": "Very short label for Public Domain images, used in a link to the file information page that has more licensing information.\n{{Identical|Public domain}}",
+ "multimediaviewer-license-default": "Short label for a link to generic license information.",
+ "multimediaviewer-permission-title": "Title of the box containing additional permission (by the copyright owner via OTRS) terms",
+ "multimediaviewer-permission-link": "Text of the link (on top of the metadata box) which shows additional permission (by the copyright owner via OTRS) terms\n\nSee also:\n* {{msg-mw|multimediaviewer-permission-link-hide}}",
+ "multimediaviewer-permission-link-hide": "Text of the link (on top of the metadata box) which hides additional permission terms\n\nSee also:\n* {{msg-mw|multimediaviewer-permission-link}}",
+ "multimediaviewer-permission-viewmore": "Text of the link (at the cutoff of the permission term preview) which shows additional permission (by the copyright owner via OTRS) terms.\n{{Identical|View more}}",
+ "multimediaviewer-restriction-2257": "Text of the tooltip for the Child Protection and Obscenity Enforcement Act restriction label, that is displayed when hovered over.",
+ "multimediaviewer-restriction-aus-reserve": "Text of the tooltip for the Australian reserve restriction label, that is displayed when hovered over.",
+ "multimediaviewer-restriction-communist": "Text of the tooltip for the communist symbol restriction label, that is displayed when hovered over.",
+ "multimediaviewer-restriction-costume": "Text of the tooltip for the costuming restriction label, that is displayed when hovered over.",
+ "multimediaviewer-restriction-currency": "Text of the tooltip for the currency restriction label, that is displayed when hovered over.",
+ "multimediaviewer-restriction-design": "Text of the tooltip for the copyrighted design restriction label, that is displayed when hovered over.",
+ "multimediaviewer-restriction-fan-art": "Text of the tooltip for the fan art restriction label, that is displayed when hovered over.",
+ "multimediaviewer-restriction-ihl": "Text of the tooltip for the IHL restriction label, that is displayed when hovered over.",
+ "multimediaviewer-restriction-insignia": "Text of the tooltip for the insignia restriction label, that is displayed when hovered over.",
+ "multimediaviewer-restriction-ita-mibac": "Text of the tooltip for the Italian MiBAC restriction label, that is displayed when hovered over.",
+ "multimediaviewer-restriction-nazi": "Text of the tooltip for the Nazi symbol restriction label, that is displayed when hovered over.",
+ "multimediaviewer-restriction-personality": "Text of the tooltip for the personality rights restriction label, that is displayed when hovered over.",
+ "multimediaviewer-restriction-trademarked": "Text of the tooltip for the trademarked restriction label, that is displayed when hovered over.",
+ "multimediaviewer-restriction-default": "Text of the tooltip for the default restriction label, that is displayed when hovered over.",
+ "multimediaviewer-restriction-default-and-others": "Text of the tooltip for the default restriction label when other restriction labels are present, that is displayed when hovered over.",
+ "multimediaviewer-about-mmv": "Text for a link to a page with more information about Media Viewer software.\n{{Identical|About}}",
+ "multimediaviewer-discuss-mmv": "Text for a link to a page where the user can discuss the Media Viewer software.\n{{Identical|Discussion}}",
+ "multimediaviewer-help-mmv": "Text for a link to a page with help about Media Viewer software.\n{{Identical|Help}}",
+ "multimediaviewer-optout-mmv": "Text for the opt-out link. Clicking on the link will turn off MediaViewer.\n\nSee also:\n* {{msg-mw|Multimediaviewer-optout-pending-mmv}}\n* {{msg-mw|Multimediaviewer-optin-mmv}}",
+ "multimediaviewer-optin-mmv": "Text for the opt-back link. Clicking it will undo the opt-out.\n\nSee also:\n* {{msg-mw|Multimediaviewer-optin-pending-mmv}}\n* {{msg-mw|Multimediaviewer-optout-mmv}}",
+ "multimediaviewer-optout-pending-mmv": "Text shown for the opt-out link while the optout request is being processed.\n\nSee also:\n* {{msg-mw|Multimediaviewer-optout-mmv}}\n* {{msg-mw|Multimediaviewer-optin-pending-mmv}}",
+ "multimediaviewer-optin-pending-mmv": "Text shown for the opt-in link while the optin request is being processed.\n\nSee also:\n* {{msg-mw|Multimediaviewer-optin-mmv}}\n* {{msg-mw|Multimediaviewer-optout-pending-mmv}}",
+ "multimediaviewer-optout-help": "Tooltip shown over the disabling link labeled {{msg-mw|Multimediaviewer-optout-mmv}}.\n\nRefers to {{msg-mw|Multimediaviewer-view-expanded}} and {{msg-mw|Multimediaviewer-optin-mmv}}.\n\nSee also:\n* {{msg-mw|Multimediaviewer-optin-help}}",
+ "multimediaviewer-optin-help": "Tooltip shown over the enabling link.\n\nThe link text is {{msg-mw|Multimediaviewer-optin-mmv}}.\n\nSee also:\n* {{msg-mw|Multimediaviewer-optout-mmv}}\n* {{msg-mw|Multimediaviewer-optout-help}}",
+ "multimediaviewer-geoloc-north": "Symbol for representing \"north\" in geolocation coordinates.\n\nUsed as <code>$4</code> in {{msg-mw|Multimediaviewer-geoloc-coord}}.",
+ "multimediaviewer-geoloc-east": "Symbol for representing \"east\" in geolocation coordinates.\n\nUsed as <code>$4</code> in {{msg-mw|Multimediaviewer-geoloc-coord}}.",
+ "multimediaviewer-geoloc-south": "Symbol for representing \"south\" in geolocation coordinates.\n\nUsed as <code>$4</code> in {{msg-mw|Multimediaviewer-geoloc-coord}}.",
+ "multimediaviewer-geoloc-west": "Symbol for representing \"west\" in geolocation coordinates.\n\nUsed as <code>$4</code> in {{msg-mw|Multimediaviewer-geoloc-coord}}.",
+ "multimediaviewer-geoloc-coord": "Format for geolocation coordinates. Parameters:\n* $1 - the number of degrees\n* $2 - the number of minutes\n* $3 - the number of seconds (rounded to the nearest hundredths place)\n* $4 - the direction symbol, defined by the following messages:\n** {{msg-mw|Multimediaviewer-geoloc-north}}\n** {{msg-mw|Multimediaviewer-geoloc-east}}\n** {{msg-mw|Multimediaviewer-geoloc-south}}\n** {{msg-mw|Multimediaviewer-geoloc-west}}",
+ "multimediaviewer-geoloc-coords": "Format for sets of geolocation coordinates. Parameters:\n* $1 - the latitude\n* $2 - the longitude\nBoth are formatted according to {{msg-mw|Multimediaviewer-geoloc-coord}}.",
+ "multimediaviewer-geolocation": "Message for displaying a location. Parameters:\n* $1 - a location which is formatted by {{msg-mw|Multimediaviewer-geoloc-coords}}\n{{Identical|Location}}",
+ "multimediaviewer-reuse-link": "Text of the link on the metadata panel which opens the reuse panel",
+ "multimediaviewer-reuse-loading-placeholder": "Text that appears in all reuse text boxes as a placeholder while the data loads.\n{{Identical|Loading}}",
+ "multimediaviewer-reuse-copy-share": "Text of the tooltip for the button to select and copy the sharing link (if supported by the browser) in the reuse panel, that is displayed when hovered over.",
+ "multimediaviewer-reuse-copy-embed": "Text of the tooltip for the button to select and copy the embedding code (if supported by the browser) in the reuse panel, that is displayed when hovered over.",
+ "multimediaviewer-share-tab": "Tab title text for the file reuse panel - used for the section with shareable URLs.\n{{Identical|Share}}",
+ "multimediaviewer-embed-tab": "Tab title text for the file reuse panel - used for the section with embeddable HTML and wikitext.",
+ "multimediaviewer-download-link": "Tooltip text for file download dialog open button.\n{{Identical|Download}}",
+ "multimediaviewer-download-preview-link-title": "Text in the link that allows the user to preview the image of the selected size - used for file download functionality.",
+ "multimediaviewer-download-original-button-name": "Text inside the button that lets the user download the original image - used for file download functionality.",
+ "multimediaviewer-download-small-button-name": "Text inside the button that lets the user download a small version of the original image - used for file download functionality.",
+ "multimediaviewer-download-medium-button-name": "Text inside the button that lets the user download a medium version of the original image - used for file download functionality.",
+ "multimediaviewer-download-large-button-name": "Text inside the button that lets the user download a large version of the original image - used for file download functionality.",
+ "multimediaviewer-link-to-page": "Used as alt-text to describe a URL that goes to a File: page for an image.",
+ "multimediaviewer-link-to-file": "Used as alt-text to describe a URL that goes to an image file.",
+ "multimediaviewer-share-explanation": "Used below the URL share input to explain what we expect the user to do.",
+ "multimediaviewer-embed-wt": "Used to represent a choice for embedding a file in a wiki page, as wikitext.\n{{Identical|Wikitext}}",
+ "multimediaviewer-embed-html": "Used to represent a choice for embedding a file in an HTML document, as HTML.\n{{Identical|HTML}}",
+ "multimediaviewer-embed-explanation": "Used below the embed textarea to explain what we expect the user to do.",
+ "multimediaviewer-text-embed-credit-text-bl": "Credit text, used when generating plain text for attributing an image - used as a caption, not in the middle of a sentence.\n\nWhich one of the multimediaviewer-text-embed-credit-text-* messages is used will depend on what information about the image is available.\n\nParameters:\n* $1 - name of the author\n* $2 - copyright tag (usually a license)\n* $3 - URL to the image source.\n{{Related|Multimediaviewer-embed-credit-text}}",
+ "multimediaviewer-text-embed-credit-text-b": "Credit text, used when generating plain text for attributing an image - used as a caption, not in the middle of a sentence.\n\nWhich one of the multimediaviewer-text-embed-credit-text-* messages is used will depend on what information about the image is available.\n\nParameters:\n* $1 - name of the author\n* $2 - URL to the image source.\n{{Related|Multimediaviewer-embed-credit-text}}",
+ "multimediaviewer-text-embed-credit-text-l": "{{optional}}\nCredit text, used when generating plain text for attributing an image - used as a caption, not in the middle of a sentence.\n\nWhich one of the multimediaviewer-text-embed-credit-text-* messages is used will depend on what information about the image is available.\n\nParameters:\n* $1 - copyright tag (usually a license)\n* $2 - URL to the image source.\n{{Related|Multimediaviewer-embed-credit-text}}",
+ "multimediaviewer-html-embed-credit-text-bl": "Credit text, used when generating HTML to reuse an image - used as a caption, not in the middle of a sentence.\n\nWhich one of the multimediaviewer-html-embed-credit-text-* messages is used will depend on what information about the image is available.\n\nParameters:\n* $1 - name of the author\n* $2 - copyright tag (usually a license)\n* $3 - URL to the image source\nEach of the parameters could be either plain text or a link.\n{{Related|Multimediaviewer-embed-credit-text}}",
+ "multimediaviewer-html-embed-credit-text-b": "Credit text, used when generating HTML to reuse an image - used as a caption, not in the middle of a sentence.\n\nWhich one of the multimediaviewer-html-embed-credit-text-* messages is used will depend on what information about the image is available.\n\nParameters:\n* $1 - name of the author\n* $2 - URL to the image source\nEach of the parameters could be either plain text or a link.\n{{Related|Multimediaviewer-embed-credit-text}}",
+ "multimediaviewer-html-embed-credit-text-l": "{{optional}}\nCredit text, used when generating HTML to reuse an image - used as a caption, not in the middle of a sentence.\n\nWhich one of the multimediaviewer-html-embed-credit-text-* messages is used will depend on what information about the image is available.\n\nParameters:\n* $1 - copyright tag (usually a license)\n* $2 - URL to the image source\nEach of the parameters could be either plain text or a link.\n{{Related|Multimediaviewer-embed-credit-text}}",
+ "multimediaviewer-html-embed-credit-link-text": "The clickable text of a link in a credit line, used when generating HTML to reuse an image.\n{{Related|Multimediaviewer-embed-credit-text}}\n{{Identical|Link}}",
+ "multimediaviewer-embed-byline": "Byline (author credit) text, used when generating wikitext/HTML to reuse the image. $1 is author name.\n{{Identical|By}}",
+ "multimediaviewer-embed-license": "License information, used when generating wikitext/HTML to reuse the image. $1 is the license name.",
+ "multimediaviewer-embed-license-nonfree": "Like {{msg-mw|multimediaviewer-embed-license}}, but for non-free images (where $1 is typically not a license name but something like \"Fair use\").",
+ "multimediaviewer-embed-via": "Source information (e.g. \"via Flickr\"), used when generating wikitext/HTML to reuse the image.\n\nParameters:\n* $1 - source (probably a website or institution name)",
+ "multimediaviewer-default-embed-dimensions": "Text of size selector option which will generate wikitext for a thumbnail without explicit size.",
+ "multimediaviewer-original-embed-dimensions": "Text of size selector option which will generate wikitext for a thumbnail with the original (full) size.\n* $1 - thumbnail dimensions, defined by the following message:\n** {{msg-mw|Multimediaviewer-embed-dimensions}}",
+ "multimediaviewer-large-embed-dimensions": "Text of size selector option which will generate wikitext for a thumbnail with small size.\n* $1 - thumbnail dimensions, defined by the following message:\n** {{msg-mw|Multimediaviewer-embed-dimensions}}\n{{Identical|Large}}",
+ "multimediaviewer-medium-embed-dimensions": "Text of size selector option which will generate wikitext for a thumbnail with medium size.\n* $1 - thumbnail dimensions, defined by the following message:\n** {{msg-mw|Multimediaviewer-embed-dimensions}}\n{{Identical|Medium}}",
+ "multimediaviewer-small-embed-dimensions": "Text of size selector option which will generate wikitext for a thumbnail with large size.\n* $1 - thumbnail dimensions, defined by the following message:\n** {{msg-mw|Multimediaviewer-embed-dimensions}}\n\n{{Identical|Small}}",
+ "multimediaviewer-embed-dimensions": "Dimensions for a given size selector option which will generate wikitext for a thumbnail.\n* $1 - width in pixels\n* $2 - height in pixels",
+ "multimediaviewer-embed-dimensions-separated": "Wraps the dimensions with a separator styled the same way, for the embed tab.\n* $1 - image dimensions\n\nSee also:\n* {{msg-mw|Multimediaviewer-embed-dimensions}}",
+ "multimediaviewer-embed-dimensions-with-file-format": "Collates image dimensions and a file format.\n* $1 - {{msg-mw|Multimediaviewer-embed-dimensions}}\n* $2 - file format extension, lowercased",
+ "multimediaviewer-description-page-button-text": "Text of the tooltip popup for the button on top of the metadata panel which links to the file description page. Used when the file was uploaded to the local wiki.\n\nSee also:\n* {{msg-mw|Multimediaviewer-description-page-popup-text}} (for remote files)",
+ "multimediaviewer-description-page-popup-text": "Text of the tooltip popup for the button on top of the metadata panel, which links to the file description page.Used when the file was uploaded to a different wiki.\n\nParameters:\n* $1 - the name of the site where the file comes from (e.g. \"Wikimedia Commons\")\nSee also:\n* {{msg-mw|Multimediaviewer-description-page-button-text}} (for local files)",
+ "multimediaviewer-commons-subtitle": "Additional text to display under {{msg-mw|multimediaviewer-repository}} when the image is from Commons.",
+ "multimediaviewer-view-expanded": "Label for a link on a file page to view the current image in the media viewer.",
+ "multimediaviewer-view-config": "Label for a link on a file page to open the Media Viewer configuration panel. This is for accessibility, and normally not visible; it is used for the icon next to the {{msg-mw|multimediaviewer-view-expanded}} button.\n{{Identical|Configuration}}",
+ "multimediaviewer-close-popup-text": "Tooltip for the button that closes the media viewer. \"Esc\" is the label of the keyboard key usually called \"[[:w:Esc key|Escape]]\".",
+ "multimediaviewer-fullscreen-popup-text": "Tooltip for a button that puts the viewer into full screen.",
+ "multimediaviewer-defullscreen-popup-text": "Tooltip for a button that exits fullscreen mode.",
+ "multimediaviewer-next-image-alt-text": "Alt text for a button that shows the next image.",
+ "multimediaviewer-prev-image-alt-text": "Alt text for a button that shows the previous image.",
+ "multimediaviewer-title-popup-text": "Tooltip that identifies the description of a file in the media viewer. (This could be the thumbnail caption, the filepage description or the file name, depending on what's available.) For very long titles, {{msg-mw|multimediaviewer-title-popup-text-more}} will be shown instead.\n{{Identical|Description}}",
+ "multimediaviewer-credit-popup-text": "Tooltip that identifies the author and source field of a file in the media viewer. If the author name or the source is very long, {{msg-mw|multimediaviewer-credit-popup-text-more}} will be shown instead.",
+ "multimediaviewer-title-popup-text-more": "Tooltip that identifies the description of a file in the media viewer. Used instead of {{msg-mw|multimediaviewer-title-popup-text}} when the name is too long and has to be truncated (and will be untruncated on click).",
+ "multimediaviewer-credit-popup-text-more": "Tooltip that identifies the author and source of a file in the media viewer. Used instead of {{msg-mw|multimediaviewer-credit-popup-text}} when one or both of the author and source is too long and has to be truncated (and will be untruncated on click).",
+ "multimediaviewer-download-attribution-cta-header": "Header for telling the user that the author of an image must be attributed, during a download action. See also {{msg-mw|multimediaviewer-download-optional-attribution-cta-header}}.",
+ "multimediaviewer-download-optional-attribution-cta-header": "Header for inviting the user to attribute author of the image during a download action. This is used for images where attribution is not a legal requirement. See also {{msg-mw|multimediaviewer-download-attribution-cta-header}}.",
+ "multimediaviewer-download-attribution-cta": "Call to action for a user to find out how to attribute the author of an image.",
+ "multimediaviewer-download-attribution-copy": "Text of the tooltip for the button to select and copy the attribution of a file (if supported by the browser) in the download panel, that is displayed when hovered over.",
+ "multimediaviewer-reuse-warning-deletion": "Warning message shown in the share/embed/download panels for files tagged with a deletion template ('''not just deleted!'''). Followed by {{msg-mw|multimediaviewer-reuse-warning-generic}}.",
+ "multimediaviewer-reuse-warning-nonfree": "Warning message shown in the share/embed/download panels for files under a nonfree license / copyright tag. Followed by {{msg-mw|multimediaviewer-reuse-warning-generic}}.",
+ "multimediaviewer-reuse-warning-noattribution": "Warning message shown in the share/embed/download panels for files which have no machine-readable author or source. Followed by {{msg-mw|multimediaviewer-reuse-warning-generic}}.",
+ "multimediaviewer-reuse-warning-generic": "Generic message at the end of every warning. $1 is the link to the file page.\n\nIt = the file",
+ "multimediaviewer-attr-plain": "Label for a button that lets the user pick plain text as an output format.",
+ "multimediaviewer-attr-html": "{{optional}}\nLabel for a button that lets the user pick HTML as an output format.\n{{Identical|HTML}}",
+ "multimediaviewer-options-tooltip": "Tooltip for a button that opens a panel for enabling or disabling the media viewer.",
+ "multimediaviewer-options-dialog-header": "Header for a dialog that gives the user the option to disable the media viewer.",
+ "multimediaviewer-options-text-header": "Text explaining the changes that happen when a user disables media viewer.",
+ "multimediaviewer-options-text-body": "Text explaining how to re-enable the media viewer after disabling.",
+ "multimediaviewer-options-learn-more": "Text for a link to more information about the media viewer.",
+ "multimediaviewer-option-submit-button": "Button for submitting a preference change that modifies the default behavior for image clicks - disables the viewer.",
+ "multimediaviewer-option-cancel-button": "Button for canceling an action on a preference change that modifies the default behavior for image clicks - closes the dialog with no action.\n{{Identical|Cancel}}",
+ "multimediaviewer-disable-confirmation-header": "Header on a dialog that informs the user they've successfully disabled the media viewer.",
+ "multimediaviewer-disable-confirmation-text": "Text informing the user that they've successfully disabled the media viewer. $1 is the wiki's name.",
+ "multimediaviewer-enable-dialog-header": "Header for a dialog that allows users to re-enable media viewer.",
+ "multimediaviewer-enable-text-header": "Text for a dialog that allows users to re-enable media viewer.",
+ "multimediaviewer-enable-submit-button": "Text for a button that re-enables media viewer.",
+ "multimediaviewer-enable-confirmation-header": "Header for a dialog that confirms that the user successfully re-enabled the media viewer.",
+ "multimediaviewer-enable-confirmation-text": "Text confirming that the user successfully re-enabled the media viewer. $1 is the wiki's name.",
+ "multimediaviewer-enable-alert": "Text shown in the enable panel to alert the user that the media viewer is currently disabled.",
+ "multimediaviewer-disable-info-title": "Used as title for {{msg-mw|Multimediaviewer-disable-info}}.",
+ "multimediaviewer-disable-info": "The title for this message is {{msg-mw|Multimediaviewer-disable-info-title}}.",
+ "multimediaviewer-errorreport-privacywarning": "A warning that is included in the Phabricator error report that's generated when the user clicks on the report link on an error page."
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/qu.json b/www/wiki/extensions/MultimediaViewer/i18n/qu.json
new file mode 100644
index 00000000..a7ac4f43
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/qu.json
@@ -0,0 +1,33 @@
+{
+ "@metadata": {
+ "authors": [
+ "AlimanRuna"
+ ]
+ },
+ "multimediaviewer-repository-local": "Astawan rikuy",
+ "multimediaviewer-datetime-created": "Kamarisqa: $1",
+ "multimediaviewer-datetime-uploaded": "Churkusqa: $1",
+ "multimediaviewer-credit-fallback": "Ruraqmanta willaykunata qhaway",
+ "multimediaviewer-license-cc-pd": "Sapsi Chaskinancha",
+ "multimediaviewer-license-pd": "Sapsi Chaskinancha",
+ "multimediaviewer-permission-title": "Imaymana saqillaymanta",
+ "multimediaviewer-permission-viewmore": "Astawan qhaway",
+ "multimediaviewer-help-mmv": "Yanapa",
+ "multimediaviewer-optout-mmv": "Multimidya qhawanaman ama niy",
+ "multimediaviewer-optin-mmv": "Multimidya qhawanata atichiy",
+ "multimediaviewer-optout-pending-mmv": "Multimidya qhawanaman ama nispa",
+ "multimediaviewer-optin-pending-mmv": "Multimidya qhawanata atichispa",
+ "multimediaviewer-optout-help": "Multimidya qhawanawanqa rikchakuna manañam rikuchisqa kanqachu. Atichinapaqqa \"{{int:multimediaviewer-view-expanded}}\" butunta ñit'iy ima rikchap kinrayninpipas. Chaymantataq \"{{int:multimediaviewer-optin-mmv}}\" nisqapi ñit'iy.",
+ "multimediaviewer-geolocation": "Kay puystupi: $1",
+ "multimediaviewer-reuse-loading-placeholder": "Chaqnamuspa…",
+ "multimediaviewer-share-tab": "Rakinakuy",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-embed-byline": "$1-pa rurasqan",
+ "multimediaviewer-original-embed-dimensions": "Qallariy willañiqi $1",
+ "multimediaviewer-large-embed-dimensions": "Hatun $1",
+ "multimediaviewer-medium-embed-dimensions": "Chawpi chhika $1",
+ "multimediaviewer-small-embed-dimensions": "Uchuy $1",
+ "multimediaviewer-description-page-popup-text": "Kay willañiqimanta astawan rikuy kaypi: $1",
+ "multimediaviewer-view-expanded": "Multimidya qhawanapi kichariy",
+ "multimediaviewer-title-popup-text": "T'iktuna"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/ro.json b/www/wiki/extensions/MultimediaViewer/i18n/ro.json
new file mode 100644
index 00000000..db65b514
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/ro.json
@@ -0,0 +1,118 @@
+{
+ "@metadata": {
+ "authors": [
+ "Minisarm",
+ "Macofe"
+ ]
+ },
+ "multimediaviewer-desc": "Extinde miniaturile la o dimensiune mai mare într-o interfață pe tot ecranul.",
+ "multimediaviewer-pref": "Vizualizator multimedia",
+ "multimediaviewer-pref-desc": "Îmbunătățiți-vă experiența de vizualizare a conținutului multimedia cu această nouă unealtă. Afișează imaginile la dimensiune mare în cadrul paginilor care conțin miniaturi. Imaginile sunt afișate într-o interfață pe tot ecranul mai simpatică, acestea putând fi, de asemenea, vizualizate la dimensiunea reală.",
+ "multimediaviewer-optin-pref": "Activează <span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About Vizualizatorul multimedia]</span>",
+ "multimediaviewer-file-page": "Du-te la pagina asociată fișierului",
+ "multimediaviewer-repository-local": "Mai multe detalii",
+ "multimediaviewer-datetime-created": "Creată: $1",
+ "multimediaviewer-datetime-uploaded": "Încărcată: $1",
+ "multimediaviewer-credit-fallback": "Vezi informații despre autor",
+ "multimediaviewer-multiple-authors": "încă {{PLURAL:$1|un autor|$1 autori|$1 de autori}}",
+ "multimediaviewer-multiple-authors-combine": "$1 și $2",
+ "multimediaviewer-metadata-error": "Nu s-au putut încărca detaliile imaginii (eroare: $1).",
+ "multimediaviewer-thumbnail-error": "Din păcate fișierul nu poate fi afișat",
+ "multimediaviewer-thumbnail-error-description": "Se pare că a apărut o eroare tehnică. Puteți să $1 sau să $3 dacă aceasta persistă. Eroare: $2",
+ "multimediaviewer-thumbnail-error-retry": "reîncercați",
+ "multimediaviewer-thumbnail-error-report": "raportați problema",
+ "multimediaviewer-license-cc-pd": "Domeniu public",
+ "multimediaviewer-license-pd": "Domeniu public",
+ "multimediaviewer-license-default": "Vezi licența",
+ "multimediaviewer-permission-title": "Detalii despre permisiuni",
+ "multimediaviewer-permission-link": "vezi condițiile",
+ "multimediaviewer-permission-link-hide": "ascunde condițiile",
+ "multimediaviewer-permission-viewmore": "Mai mult",
+ "multimediaviewer-restriction-2257": "Această imagine ilustrează conținut sexual explicit, care poate fi subiectul legii de Protecție a copilului și luptei împotriva obscenității din Statele Unite.",
+ "multimediaviewer-restriction-aus-reserve": "Această fotografie a fost surprinsă într-o rezervație a Commonwealth-ului Australiei și nu poate fi utilizată în scopuri comerciale fără permisiune.",
+ "multimediaviewer-restriction-communist": "Această imagine conține însemne ale comunismului, care pot fi ilegale în anumite țări.",
+ "multimediaviewer-restriction-costume": "Această imagine ilustrează costumații și poate face subiectul restricțiilor legale.",
+ "multimediaviewer-restriction-currency": "Această imagine reprezintă ilustrația unei unități monetare și poate face subiectul restricțiilor legale.",
+ "multimediaviewer-restriction-design": "Designul subiectului din această imagine se poate afla sub incidența drepturilor de autor și poate face subiectul restricțiilor legale.",
+ "multimediaviewer-restriction-fan-art": "Această imagine este o operă de fan art, iar reutilizarea sa poate face subiectul restricțiilor legale.",
+ "multimediaviewer-restriction-ihl": "Această imagine conține simboluri restricționate de Dreptul internațional umanitar.",
+ "multimediaviewer-restriction-insignia": "Această imagine conține însemne oficiale, care pot face subiectul restricțiilor legale.",
+ "multimediaviewer-restriction-ita-mibac": "Această imagine reproduce o proprietate aparținând patrimoniului cultural italian și este restricționată de legislația italiană.",
+ "multimediaviewer-restriction-nazi": "Această imagine conține însemne naziste sau fasciste, care pot fi ilegale în anumite țări.",
+ "multimediaviewer-restriction-personality": "Această imagine poate ilustra persoane ale căror drepturi legale restricționează anumite reutilizări ale imaginii fără consimțământul acestora.",
+ "multimediaviewer-restriction-trademarked": "Conținutul acestei imagini poate fi subiectul legilor privitoare la mărcile înregistrate.",
+ "multimediaviewer-restriction-default": "Această imagine poate fi restricționată prin dispoziții legale în afara legii drepturilor de autor. Consultați pagina descriptivă a fișierului pentru detalii.",
+ "multimediaviewer-restriction-default-and-others": "Această imagine poate fi restricționată și prin alte dispoziții legale în afara legii drepturilor de autor. Consultați pagina descriptivă a fișierului pentru detalii.",
+ "multimediaviewer-about-mmv": "Despre",
+ "multimediaviewer-discuss-mmv": "Discuții",
+ "multimediaviewer-help-mmv": "Ajutor",
+ "multimediaviewer-optout-mmv": "Dezactivează Vizualizatorul multimedia",
+ "multimediaviewer-optin-mmv": "Activează Vizualizatorul multimedia",
+ "multimediaviewer-optout-pending-mmv": "Se dezactivează Vizualizatorul multimedia",
+ "multimediaviewer-optin-pending-mmv": "Se activează Vizualizatorul multimedia",
+ "multimediaviewer-optout-help": "Vizualizatorul multimedia nu va mai fi utilizat pentru afișarea imaginilor. Pentru a-l folosi din nou, apăsați butonul „{{int:multimediaviewer-view-expanded}}”, care se găsește în dreptul oricărei imagini. Apoi faceți clic pe „{{int:multimediaviewer-optin-mmv}}”.",
+ "multimediaviewer-optin-help": "Vizualizatorul multimedia va fi folosit pentru afișarea imaginilor.",
+ "multimediaviewer-geolocation": "Poziție: $1",
+ "multimediaviewer-reuse-link": "Distribuie sau încorporează acest fișier",
+ "multimediaviewer-reuse-loading-placeholder": "Se încarcă...",
+ "multimediaviewer-share-tab": "Distribuire",
+ "multimediaviewer-embed-tab": "Încorporare",
+ "multimediaviewer-download-link": "Descarcă acest fișier",
+ "multimediaviewer-download-preview-link-title": "Vizualizează cu navigatorul",
+ "multimediaviewer-download-original-button-name": "Descarcă fișierul original",
+ "multimediaviewer-download-small-button-name": "Descarcă la dimensiune mică",
+ "multimediaviewer-download-medium-button-name": "Descarcă la dimensiune intermediară",
+ "multimediaviewer-download-large-button-name": "Descarcă la dimensiune mare",
+ "multimediaviewer-link-to-page": "Legătură către pagina descriptivă a fișierului",
+ "multimediaviewer-link-to-file": "Legătură către fișierul original",
+ "multimediaviewer-share-explanation": "Copiați legătura și distribuiți-o liber",
+ "multimediaviewer-embed-wt": "Text wiki",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-embed-explanation": "Utilizați acest cod pentru a încorpora fișierul",
+ "multimediaviewer-text-embed-credit-text-bl": "De la $1, $2, $3",
+ "multimediaviewer-text-embed-credit-text-b": "De la $1, $2",
+ "multimediaviewer-html-embed-credit-text-bl": "De la $1, $2, $3",
+ "multimediaviewer-html-embed-credit-text-b": "De la $1, $2",
+ "multimediaviewer-embed-byline": "De $1",
+ "multimediaviewer-embed-license": "Sub licență $1.",
+ "multimediaviewer-embed-via": "Via $1.",
+ "multimediaviewer-default-embed-dimensions": "Dimensiunea implicită a miniaturii",
+ "multimediaviewer-original-embed-dimensions": "Fișier original $1",
+ "multimediaviewer-large-embed-dimensions": "Mare $1",
+ "multimediaviewer-medium-embed-dimensions": "Intermediară $1",
+ "multimediaviewer-small-embed-dimensions": "Mică $1",
+ "multimediaviewer-description-page-button-text": "Mai multe detalii despre acest fișier",
+ "multimediaviewer-description-page-popup-text": "Mai multe detalii despre acest fișier la $1",
+ "multimediaviewer-commons-subtitle": "Depozitul liber de conținut media",
+ "multimediaviewer-view-expanded": "Deschide cu Vizualizatorul multimedia",
+ "multimediaviewer-view-config": "Configurare",
+ "multimediaviewer-close-popup-text": "Închide această unealtă (Esc)",
+ "multimediaviewer-fullscreen-popup-text": "Afișează pe tot ecranul",
+ "multimediaviewer-defullscreen-popup-text": "Ieșire din ecran complet",
+ "multimediaviewer-title-popup-text": "Descriere",
+ "multimediaviewer-credit-popup-text": "Informații despre autor și sursă",
+ "multimediaviewer-title-popup-text-more": "Vezi descrierea completă",
+ "multimediaviewer-credit-popup-text-more": "Vezi informațiile complete despre autor şi sursă",
+ "multimediaviewer-download-attribution-cta-header": "Trebuie să atribuiți autorului opera sa",
+ "multimediaviewer-download-optional-attribution-cta-header": "Puteți atribui autorului opera sa",
+ "multimediaviewer-download-attribution-cta": "Arată-mi cum",
+ "multimediaviewer-attr-plain": "Simplu",
+ "multimediaviewer-options-tooltip": "Activează sau dezactivează Vizualizatorul multimedia",
+ "multimediaviewer-options-dialog-header": "Dezactivați Vizualizatorul multimedia?",
+ "multimediaviewer-options-text-header": "Ignoră această opțiune de vizualizare pentru toate fișierele.",
+ "multimediaviewer-options-text-body": "Îl puteți activa mai târziu din pagina descriptivă a fișierelor.",
+ "multimediaviewer-options-learn-more": "Aflați mai multe",
+ "multimediaviewer-option-submit-button": "Dezactivează Vizualizatorul multimedia",
+ "multimediaviewer-option-cancel-button": "Revocare",
+ "multimediaviewer-disable-confirmation-header": "Ați dezactivat Vizualizatorul multimedia",
+ "multimediaviewer-disable-confirmation-text": "Data viitoare când veți face clic pe o miniatură la $1, veți accesa direct detaliile fișierului.",
+ "multimediaviewer-enable-dialog-header": "Activați Vizualizatorul multimedia?",
+ "multimediaviewer-enable-text-header": "Activează această opțiune de vizualizare multimedia în mod implicit pentru toate fișierele.",
+ "multimediaviewer-enable-submit-button": "Activează Vizualizatorul multimedia",
+ "multimediaviewer-enable-confirmation-header": "Ați activat Vizualizatorul multimedia pentru toate fișierele",
+ "multimediaviewer-enable-confirmation-text": "Data viitoare când veți face clic pe o miniatură la $1, se va lansa Vizualizatorul multimedia.",
+ "multimediaviewer-enable-alert": "Vizualizatorul multimedia este acum dezactivat",
+ "multimediaviewer-disable-info-title": "Ați dezactivat Vizualizatorul multimedia",
+ "multimediaviewer-disable-info": "Puteți vizualiza în continuare fișiere individuale cu Vizualizatorul multimedia.",
+ "multimediaviewer-errorreport-privacywarning": "Detaliile referitoare la eroare sunt atașate raportului care va fi făcut public. Dacă nu vă simțiți confortabil în această postură, puteți modifica raportul și să eliminați informațiile pe care nu le doriți publice."
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/roa-tara.json b/www/wiki/extensions/MultimediaViewer/i18n/roa-tara.json
new file mode 100644
index 00000000..9049a635
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/roa-tara.json
@@ -0,0 +1,11 @@
+{
+ "@metadata": {
+ "authors": [
+ "Joetaras"
+ ]
+ },
+ "multimediaviewer-optin-pref": "Abbilite 'u <span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About Media Viewer]</span>",
+ "multimediaviewer-thumbnail-error-retry": "pruève arrete",
+ "multimediaviewer-license-pd": "Dominie pubbleche",
+ "multimediaviewer-geolocation": "Posizione: $1"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/ru.json b/www/wiki/extensions/MultimediaViewer/i18n/ru.json
new file mode 100644
index 00000000..7f3645f7
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/ru.json
@@ -0,0 +1,122 @@
+{
+ "@metadata": {
+ "authors": [
+ "Kaganer",
+ "Okras",
+ "Tucvbif",
+ "Meshkov.a",
+ "Wizardist",
+ "Striking Blue",
+ "Dağlı95",
+ "Macofe",
+ "Jack who built the house",
+ "Kareyac",
+ "Putnik",
+ "Smigles"
+ ]
+ },
+ "multimediaviewer-desc": "Раскрывает эскизы в большие изображения на весь экран.",
+ "multimediaviewer-pref": "Просмотрщик медиафайлов",
+ "multimediaviewer-pref-desc": "Улучшает просмотр мультимедиа-файлов новым инструментом. На странице с эскизами изображений он раскрывает эскизы в большие изображения. Изображения показываются в более красивом полноэкранном интерфейсе, а также могут быть открыты в оригинальном разрешении.",
+ "multimediaviewer-optin-pref": "Включить <span class=\"plainlinks\">«[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About Просмотрщик медиафайлов]</span>»",
+ "multimediaviewer-file-page": "Перейти на страницу соответствующего файла",
+ "multimediaviewer-repository-local": "Подробнее",
+ "multimediaviewer-datetime-created": "Создано: $1",
+ "multimediaviewer-datetime-uploaded": "Загружено: $1",
+ "multimediaviewer-credit-fallback": "Посмотреть информацию об авторе",
+ "multimediaviewer-multiple-authors": "ещё {{PLURAL:$1|один автор|$1 авторов|$1 автора}}",
+ "multimediaviewer-multiple-authors-combine": "$1 и $2",
+ "multimediaviewer-metadata-error": "Не удалось загрузить данные изображения (ошибка: $1)",
+ "multimediaviewer-thumbnail-error": "Извините, файл не может быть отображён",
+ "multimediaviewer-thumbnail-error-description": "Похоже, возникла какая-то техническая проблема. Вы можете $1 или $3, если он будет повторяться. Ошибка: $2",
+ "multimediaviewer-thumbnail-error-retry": "повторить",
+ "multimediaviewer-thumbnail-error-report": "сообщить о проблеме",
+ "multimediaviewer-license-cc-pd": "Общественное достояние",
+ "multimediaviewer-license-pd": "Общественное достояние",
+ "multimediaviewer-license-default": "Просмотр лицензии",
+ "multimediaviewer-permission-title": "Сведения о разрешении",
+ "multimediaviewer-permission-link": "просмотр условий",
+ "multimediaviewer-permission-link-hide": "скрыть условия",
+ "multimediaviewer-permission-viewmore": "Посмотреть подробнее",
+ "multimediaviewer-restriction-2257": "Это изображение содержит откровенное сексуального содержание, которое может регулироваться в США Актом по защите детей и борьбе с непристойностью.",
+ "multimediaviewer-restriction-aus-reserve": "Это изображение было сделано в заповеднике Австралийского содружества и не может быть использовано в коммерческих целях без разрешения.",
+ "multimediaviewer-restriction-communist": "Это изображение содержит коммунистическую символику, которая может быть запрещена в некоторых странах.",
+ "multimediaviewer-restriction-currency": "Это изображение представляет собой изображение денежной единицы и может быть подвержено правовым ограничениям.",
+ "multimediaviewer-restriction-ihl": "Это изображение содержит символы, ограниченные международным гуманитарным правом.",
+ "multimediaviewer-restriction-insignia": "Это изображение содержит официальные знаки отличия, которые могут быть объектом правового ограничения.",
+ "multimediaviewer-restriction-trademarked": "Это изображение содержит материал, который может быть объектом законов о товарном знаке.",
+ "multimediaviewer-about-mmv": "О просмотрщике",
+ "multimediaviewer-discuss-mmv": "Обсуждение",
+ "multimediaviewer-help-mmv": "Справка",
+ "multimediaviewer-optout-mmv": "Отключить «Просмотрщик медиафайлов»",
+ "multimediaviewer-optin-mmv": "Включить «Просмотрщик медиафайлов»",
+ "multimediaviewer-optout-pending-mmv": "Отключение «Просмотрщика медиафайлов»",
+ "multimediaviewer-optin-pending-mmv": "Включение «Просмотрщика медиафайлов»",
+ "multimediaviewer-optout-help": "«Просмотрщик медиафайлов» больше не будет использоваться для показа изображений. Чтобы использовать его снова, нажмите на кнопку «{{int:multimediaviewer-view-expanded}}» рядом с любой образ. Затем нажмите на «{{int:multimediaviewer-optin-mmv}}».",
+ "multimediaviewer-optin-help": "Для показа изображений будет использоваться «Просмотрщик медиафайлов».",
+ "multimediaviewer-geolocation": "Географическое положение: $1",
+ "multimediaviewer-reuse-link": "Поделиться или встроить этот файл",
+ "multimediaviewer-reuse-loading-placeholder": "Загрузка…",
+ "multimediaviewer-share-tab": "Поделиться",
+ "multimediaviewer-embed-tab": "Вставить на страницу",
+ "multimediaviewer-download-link": "Загрузить этот файл",
+ "multimediaviewer-download-preview-link-title": "Просмотр в браузере",
+ "multimediaviewer-download-original-button-name": "Скачать исходный файл",
+ "multimediaviewer-download-small-button-name": "Скачать в маленьком размере",
+ "multimediaviewer-download-medium-button-name": "Скачать в среднем размере",
+ "multimediaviewer-download-large-button-name": "Скачать в большом размере",
+ "multimediaviewer-link-to-page": "Ссылка на страницу описания файла",
+ "multimediaviewer-link-to-file": "Ссылка на исходный файл",
+ "multimediaviewer-share-explanation": "Скопируйте и свободно делитесь ссылкой",
+ "multimediaviewer-embed-wt": "Вики-текст",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-embed-explanation": "Используйте этот код, чтобы вставить файл на страницу",
+ "multimediaviewer-text-embed-credit-text-bl": "Автор: $1, $2, $3",
+ "multimediaviewer-text-embed-credit-text-b": "Автор: $1, $2",
+ "multimediaviewer-html-embed-credit-text-bl": "Автор: $1, $2, $3",
+ "multimediaviewer-html-embed-credit-text-b": "Автор: $1, $2",
+ "multimediaviewer-html-embed-credit-link-text": "Ссылка",
+ "multimediaviewer-embed-byline": "Участника $1",
+ "multimediaviewer-embed-license": "Под лицензией $1.",
+ "multimediaviewer-embed-via": "Через $1.",
+ "multimediaviewer-default-embed-dimensions": "Размер эскиза по умолчанию",
+ "multimediaviewer-original-embed-dimensions": "Исходный файл $1",
+ "multimediaviewer-large-embed-dimensions": "Большой $1",
+ "multimediaviewer-medium-embed-dimensions": "Средний $1",
+ "multimediaviewer-small-embed-dimensions": "Маленький $1",
+ "multimediaviewer-description-page-button-text": "Подробнее об этом файле",
+ "multimediaviewer-description-page-popup-text": "Подробнее об этом файле на сайте $1",
+ "multimediaviewer-commons-subtitle": "Хранилище свободных медиафайлов",
+ "multimediaviewer-view-expanded": "Открыть в «Просмотрщике медиафайлов»",
+ "multimediaviewer-view-config": "Настройка",
+ "multimediaviewer-close-popup-text": "Закрыть этот инструмент (Esc)",
+ "multimediaviewer-fullscreen-popup-text": "Показать в полноэкранном режиме",
+ "multimediaviewer-defullscreen-popup-text": "Выход из полноэкранного режима",
+ "multimediaviewer-next-image-alt-text": "Показать следующее изображение",
+ "multimediaviewer-prev-image-alt-text": "Показать предыдущее изображение",
+ "multimediaviewer-title-popup-text": "Описание",
+ "multimediaviewer-credit-popup-text": "Информация об авторе и источнике",
+ "multimediaviewer-title-popup-text-more": "Посмотреть полное описание",
+ "multimediaviewer-credit-popup-text-more": "Посмотреть полную информацию об авторе и источнике",
+ "multimediaviewer-download-attribution-cta-header": "Вам нужно указать автора",
+ "multimediaviewer-download-optional-attribution-cta-header": "Вам нужно указать автора",
+ "multimediaviewer-download-attribution-cta": "Показать мне, как",
+ "multimediaviewer-attr-plain": "Простой",
+ "multimediaviewer-options-tooltip": "Включение или отключение «Просмотрщика медиафайлов»",
+ "multimediaviewer-options-dialog-header": "Отключить «Просмотрщик медиафайлов»?",
+ "multimediaviewer-options-text-header": "Выключить эту функцию просмотра для всех файлов.",
+ "multimediaviewer-options-text-body": "Вы можете включить его позже на странице сведений о файле.",
+ "multimediaviewer-options-learn-more": "Узнать больше",
+ "multimediaviewer-option-submit-button": "Отключить «Просмотрщик медиафайлов»",
+ "multimediaviewer-option-cancel-button": "Отмена",
+ "multimediaviewer-disable-confirmation-header": "Вы отключили «Просмотрщик медиафайлов»",
+ "multimediaviewer-disable-confirmation-text": "Следующий раз, когда вы щёлкните на миниатюре файла на сайте $1, вы напрямую сможете просматривать все сведения о файле.",
+ "multimediaviewer-enable-dialog-header": "Включить «Просмотрщик медиафайлов»?",
+ "multimediaviewer-enable-text-header": "Включить эту функцию медиапросмотра по умолчанию для всех файлов.",
+ "multimediaviewer-enable-submit-button": "Включить «Просмотрщик медиафайлов»",
+ "multimediaviewer-enable-confirmation-header": "Вы включили «Просмотрщик медиафайлов» для всех файлов",
+ "multimediaviewer-enable-confirmation-text": "Следующий раз, когда вы щёлкните на миниатюре файла на сайте $1, будет использован «Просмотрщик медиафайлов».",
+ "multimediaviewer-enable-alert": "«Просмотрщик медиафайлов» теперь отключен",
+ "multimediaviewer-disable-info-title": "Вы отключили «Просмотрщик медиафайлов»",
+ "multimediaviewer-disable-info": "Вы всё ещё можете просматривать отдельные файлы через «Просмотрщик медиафайлов»."
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/sa.json b/www/wiki/extensions/MultimediaViewer/i18n/sa.json
new file mode 100644
index 00000000..42a264ff
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/sa.json
@@ -0,0 +1,18 @@
+{
+ "@metadata": {
+ "authors": [
+ "NehalDaveND"
+ ]
+ },
+ "multimediaviewer-repository-local": "अधिकविवरणम्",
+ "multimediaviewer-multiple-authors-combine": "$1, $2 च",
+ "multimediaviewer-thumbnail-error-retry": "पुनःप्रयासः",
+ "multimediaviewer-help-mmv": "साहाय्यम्",
+ "multimediaviewer-reuse-loading-placeholder": "आरोपयति...",
+ "multimediaviewer-share-tab": "वितरतु",
+ "multimediaviewer-embed-html": "एच् टि एम् एल्",
+ "multimediaviewer-embed-byline": "$1 द्वारा",
+ "multimediaviewer-embed-via": "$1 भूत्वा",
+ "multimediaviewer-title-popup-text": "वर्णनम्",
+ "multimediaviewer-option-cancel-button": "निरस्यताम्"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/sco.json b/www/wiki/extensions/MultimediaViewer/i18n/sco.json
new file mode 100644
index 00000000..e34e8cf2
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/sco.json
@@ -0,0 +1,44 @@
+{
+ "@metadata": {
+ "authors": [
+ "John Reid",
+ "AmaryllisGardener"
+ ]
+ },
+ "multimediaviewer-optin-pref": "Enable <span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About Media Viewer]</span>",
+ "multimediaviewer-metadata-error": "Mistak: Coudna laid eemage data. $1",
+ "multimediaviewer-thumbnail-error": "Mistak: Coudna laid thummnail data. $1",
+ "multimediaviewer-license-pd": "Public Domain",
+ "multimediaviewer-permission-title": "Permeession details",
+ "multimediaviewer-permission-link": "see the terms",
+ "multimediaviewer-permission-viewmore": "See mair",
+ "multimediaviewer-discuss-mmv": "Tauk ower this featur",
+ "multimediaviewer-help-mmv": "Heelp",
+ "multimediaviewer-optin-mmv": "Enable Media Viewer",
+ "multimediaviewer-optout-pending-mmv": "Disablin Media Viewer",
+ "multimediaviewer-optin-pending-mmv": "Enablin Media Viewer",
+ "multimediaviewer-optout-help": "Media Viewer wil no be uised tae shaw eemages onie mair. Tae uise it again, clap oan the \"{{int:mutimediaviewer-view-expanded}}\" button nex tae onie eemage. Than clap oan \"{{int:multimediaviewer-optin-mmv}}\".",
+ "multimediaviewer-reuse-loading-placeholder": "Laidin...",
+ "multimediaviewer-share-tab": "Shair",
+ "multimediaviewer-embed-tab": "Embed",
+ "multimediaviewer-download-link": "Dounlaid this file",
+ "multimediaviewer-download-preview-link-title": "See in the brouser",
+ "multimediaviewer-download-original-button-name": "Dounlaid the oreeginal file",
+ "multimediaviewer-download-small-button-name": "Dounlaid wee size",
+ "multimediaviewer-download-medium-button-name": "Dounlaid middlin size",
+ "multimediaviewer-download-large-button-name": "Dounlaid muckle size",
+ "multimediaviewer-link-to-page": "Link til file descreeption page",
+ "multimediaviewer-link-to-file": "Airt til oreeginal file",
+ "multimediaviewer-share-explanation": "Capie n freelie shair the airtin",
+ "multimediaviewer-embed-wt": "Wikitex",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-embed-explanation": "Uise this code tae embed the file",
+ "multimediaviewer-embed-byline": "Bi $1",
+ "multimediaviewer-embed-license": "Licensed unner $1.",
+ "multimediaviewer-embed-via": "Bi waa o $1.",
+ "multimediaviewer-default-embed-dimensions": "Defaut thummnail size",
+ "multimediaviewer-original-embed-dimensions": "Oreeginal file $1",
+ "multimediaviewer-large-embed-dimensions": "Muckle $1",
+ "multimediaviewer-medium-embed-dimensions": "Midlin $1",
+ "multimediaviewer-small-embed-dimensions": "Smaw $1"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/sd.json b/www/wiki/extensions/MultimediaViewer/i18n/sd.json
new file mode 100644
index 00000000..3f6f3071
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/sd.json
@@ -0,0 +1,12 @@
+{
+ "@metadata": {
+ "authors": [
+ "Mehtab ahmed",
+ "Aursani"
+ ]
+ },
+ "multimediaviewer-thumbnail-error-description": "ڪو ٽيڪنيڪي مسئلو ٿيو آهي. جي حل نہ ٿي تہ توهان $1 يا $3 ڪري سگھو ٿا. چُڪَ: $2",
+ "multimediaviewer-thumbnail-error-report": "مسئلي جي رپورٽ ڪريو",
+ "multimediaviewer-about-mmv": "بابت",
+ "multimediaviewer-discuss-mmv": "بحث"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/sdc.json b/www/wiki/extensions/MultimediaViewer/i18n/sdc.json
new file mode 100644
index 00000000..620924ca
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/sdc.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Jun Misugi"
+ ]
+ },
+ "multimediaviewer-repository-local": "Imparà più"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/sgs.json b/www/wiki/extensions/MultimediaViewer/i18n/sgs.json
new file mode 100644
index 00000000..cafc90e9
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/sgs.json
@@ -0,0 +1,25 @@
+{
+ "@metadata": {
+ "authors": [
+ "Hugo.arg"
+ ]
+ },
+ "multimediaviewer-pref": "Abruozdieliu parveiza",
+ "multimediaviewer-optin-pref": "Ijongtė <span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About abruozdieliu parvaizas rakonda]</span>",
+ "multimediaviewer-repository-local": "Tėksliau",
+ "multimediaviewer-datetime-created": "Padėrbts: $1",
+ "multimediaviewer-datetime-uploaded": "Ikelts: $1",
+ "multimediaviewer-credit-fallback": "Veizietė žėnēs aple autorio",
+ "multimediaviewer-multiple-authors-combine": "$1 ė $2",
+ "multimediaviewer-license-cc-pd": "Vėiša nauduojėma",
+ "multimediaviewer-license-pd": "Vėiša nauduojėma",
+ "multimediaviewer-license-default": "Veizietė lėcencėjė",
+ "multimediaviewer-permission-link": "veizietė sālīgas",
+ "multimediaviewer-permission-link-hide": "kavuotė sālīgas",
+ "multimediaviewer-permission-viewmore": "Da veizietė",
+ "multimediaviewer-help-mmv": "Pagelba",
+ "multimediaviewer-geolocation": "Vėita: $1",
+ "multimediaviewer-description-page-button-text": "Daugiau žėniū aple ta abruozdieli",
+ "multimediaviewer-view-expanded": "Atverė so abruozdieliu parveiza",
+ "multimediaviewer-title-popup-text": "Aprašīms"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/si.json b/www/wiki/extensions/MultimediaViewer/i18n/si.json
new file mode 100644
index 00000000..bd57cfef
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/si.json
@@ -0,0 +1,18 @@
+{
+ "@metadata": {
+ "authors": [
+ "Sahan.ssw",
+ "Susith Chandira Gts",
+ "Thirsty",
+ 1100100
+ ]
+ },
+ "multimediaviewer-metadata-error": "පින්තූරයේ විස්තර වලට ප්‍රෙව්ශනය විය නොහැක(දෝෂය:$1)",
+ "multimediaviewer-thumbnail-error": "සමාවන්න, ගොනුව පෙන්විය නොහැක.",
+ "multimediaviewer-thumbnail-error-retry": "නැවත උත්සහ කරන්න",
+ "multimediaviewer-permission-title": "බලපත්‍ර විස්තර",
+ "multimediaviewer-permission-link": "කොන්දේසි පෙන්වන්න",
+ "multimediaviewer-permission-link-hide": "කොන්දේසි සඟවන්න",
+ "multimediaviewer-permission-viewmore": "තවත් බලන්න",
+ "multimediaviewer-embed-html": "HTML"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/sk.json b/www/wiki/extensions/MultimediaViewer/i18n/sk.json
new file mode 100644
index 00000000..ed5215ff
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/sk.json
@@ -0,0 +1,71 @@
+{
+ "@metadata": {
+ "authors": [
+ "Teslaton",
+ "LacoR"
+ ]
+ },
+ "multimediaviewer-desc": "Zobrazí náhľady obrázkov na celú obrazovku.",
+ "multimediaviewer-pref": "Prehliadač médií",
+ "multimediaviewer-optin-pref": "Zapnúť <span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About Prehliadač médií]</span>",
+ "multimediaviewer-repository-local": "Viac informácií",
+ "multimediaviewer-datetime-created": "Vytvorené: $1",
+ "multimediaviewer-datetime-uploaded": "Nahrané: $1",
+ "multimediaviewer-credit-fallback": "Zobraziť informácie o autorovi",
+ "multimediaviewer-multiple-authors": "{{PLURAL:$1|ďalší autor|$1 ďalší autori|$1 ďalších autorov}}",
+ "multimediaviewer-multiple-authors-combine": "$1 a $2",
+ "multimediaviewer-metadata-error": "Nedajú sa načítať podrobnosti obrázka (chyba: $1)",
+ "multimediaviewer-thumbnail-error": "Prepáčte, súbor nie je možné zobraziť",
+ "multimediaviewer-thumbnail-error-retry": "znova",
+ "multimediaviewer-thumbnail-error-report": "nahlásiť problém",
+ "multimediaviewer-license-cc-pd": "Voľné dielo",
+ "multimediaviewer-license-pd": "Voľné dielo",
+ "multimediaviewer-license-default": "Zobraziť licenciu",
+ "multimediaviewer-permission-title": "Podrobnosti o povolení",
+ "multimediaviewer-permission-link": "zobraziť podmienky",
+ "multimediaviewer-permission-link-hide": "skryť podmienky",
+ "multimediaviewer-permission-viewmore": "Zobraziť viac",
+ "multimediaviewer-restriction-2257": "Tento obrázok obsahuje sexuálne explicitný obsah, ktorý môže byť predmetom zákona Spojených štátov Child Protection and Obscenity Enforcement Act.",
+ "multimediaviewer-restriction-aus-reserve": "Táto fotka bola vytvorená v austrálskej prírodnej rezervácii a nesmie byť použitá na komerčné účely bez povolenia.",
+ "multimediaviewer-restriction-communist": "Tento obrázok obsahuje komunistické symboly, ktoré môžu byť v niektorých krajinách zakázané.",
+ "multimediaviewer-restriction-costume": "Tento obrázok zobrazuje kostýmy a môže byť predmetom zákonných obmedzení.",
+ "multimediaviewer-restriction-nazi": "Tento obrázok obsahuje nacistické alebo iné fašistické symboly, ktoré môžu byť v niektorých krajinách zakázané.",
+ "multimediaviewer-discuss-mmv": "Diskusia",
+ "multimediaviewer-help-mmv": "Pomoc",
+ "multimediaviewer-optout-mmv": "Vypnúť Prehliadač médií",
+ "multimediaviewer-optin-mmv": "Zapnúť Prehliadač médií",
+ "multimediaviewer-optout-pending-mmv": "Prehliadač médií sa vypína",
+ "multimediaviewer-optin-pending-mmv": "Prehliadač médií sa zapína",
+ "multimediaviewer-geolocation": "Poloha: $1",
+ "multimediaviewer-reuse-link": "Zdielať alebo vložiť tento súbor",
+ "multimediaviewer-reuse-loading-placeholder": "Načítavam…",
+ "multimediaviewer-share-tab": "Zdieľať",
+ "multimediaviewer-embed-tab": "Vložiť",
+ "multimediaviewer-download-link": "Stiahnuť tento súbor",
+ "multimediaviewer-download-preview-link-title": "Otvoriť v prehliadači",
+ "multimediaviewer-download-original-button-name": "Stiahnuť pôvodný súbor",
+ "multimediaviewer-text-embed-credit-text-bl": "Autor: $1, $2, $3",
+ "multimediaviewer-text-embed-credit-text-b": "Autor: $1, $2",
+ "multimediaviewer-html-embed-credit-text-bl": "Autor: $1, $2, $3",
+ "multimediaviewer-html-embed-credit-text-b": "Autor: $1, $2",
+ "multimediaviewer-embed-byline": "Autor: $1",
+ "multimediaviewer-embed-license": "Licencovaný pod $1.",
+ "multimediaviewer-embed-via": "Prostredníctvom $1.",
+ "multimediaviewer-default-embed-dimensions": "Predvolená veľkosť náhľadu",
+ "multimediaviewer-original-embed-dimensions": "Pôvodný súbor $1",
+ "multimediaviewer-description-page-button-text": "Viac informácií o tomto súbore",
+ "multimediaviewer-description-page-popup-text": "Viac informácií o tomto súbore na $1",
+ "multimediaviewer-view-expanded": "Zobraziť v prehliadači médií",
+ "multimediaviewer-view-config": "Nastavenie",
+ "multimediaviewer-close-popup-text": "Zatvoriť tento nástroj (Esc)",
+ "multimediaviewer-fullscreen-popup-text": "Zobraziť na celej obrazovke",
+ "multimediaviewer-defullscreen-popup-text": "Ukončiť režim zobrazenia na celej obrazovky",
+ "multimediaviewer-title-popup-text": "Popis",
+ "multimediaviewer-credit-popup-text": "Informácie o autorovi a zdroji",
+ "multimediaviewer-title-popup-text-more": "Zobraziť úplný popis",
+ "multimediaviewer-credit-popup-text-more": "Zobraziť úplné informácie o autorovi a zdroji",
+ "multimediaviewer-download-attribution-cta-header": "Musíte uviesť autora",
+ "multimediaviewer-download-optional-attribution-cta-header": "Môžete uviesť autora",
+ "multimediaviewer-download-attribution-cta": "Ukážte mi ako",
+ "multimediaviewer-option-submit-button": "Vypnúť Prehliadač médií"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/sl.json b/www/wiki/extensions/MultimediaViewer/i18n/sl.json
new file mode 100644
index 00000000..82f253c2
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/sl.json
@@ -0,0 +1,50 @@
+{
+ "@metadata": {
+ "authors": [
+ "Dbc334",
+ "Eleassar",
+ "Pickle12"
+ ]
+ },
+ "multimediaviewer-desc": "Razširitev sličic v večji velikosti v celozaslonskem vmesniku.",
+ "multimediaviewer-pref": "Predstavnostni pregledovalnik",
+ "multimediaviewer-pref-desc": "S tem orodjem lahko izboljšate svojo izkušnjo pri ogledovanju večpredstavnostnih vsebin. Orodje prikazuje slike na straneh s sličicami v večji velikosti in v lepšem okvirčku celozaslonskega vmesnika, mogoč pa je tudi celozaslonski prikaz.",
+ "multimediaviewer-file-page": "Pojdi na pripadajočo opisno stran datoteke.",
+ "multimediaviewer-repository-local": "Več podrobnosti",
+ "multimediaviewer-datetime-created": "Ustvarjeno: $1",
+ "multimediaviewer-datetime-uploaded": "Naloženo: $1",
+ "multimediaviewer-license-cc-pd": "javna last",
+ "multimediaviewer-license-pd": "Javna last",
+ "multimediaviewer-license-default": "Prikaz licence",
+ "multimediaviewer-permission-title": "Podatki o dovoljenju",
+ "multimediaviewer-permission-link": "ogled pogojev",
+ "multimediaviewer-permission-viewmore": "Oglej si več",
+ "multimediaviewer-about-mmv": "O pregledovalniku",
+ "multimediaviewer-discuss-mmv": "Razprava",
+ "multimediaviewer-help-mmv": "Pomoč",
+ "multimediaviewer-geolocation": "Lokacija: $1",
+ "multimediaviewer-reuse-link": "Deli ali vdelaj datoteko",
+ "multimediaviewer-reuse-loading-placeholder": "Nalaganje ...",
+ "multimediaviewer-share-tab": "Deli",
+ "multimediaviewer-embed-tab": "Vdelaj",
+ "multimediaviewer-download-link": "Prenesi datoteko",
+ "multimediaviewer-download-preview-link-title": "Ogled v brskalniku",
+ "multimediaviewer-download-original-button-name": "Prenesi izvirno datoteko",
+ "multimediaviewer-download-small-button-name": "Prenesi majhno velikost",
+ "multimediaviewer-download-medium-button-name": "Prenesi srednjo velikost",
+ "multimediaviewer-download-large-button-name": "Prenesi veliko velikost",
+ "multimediaviewer-link-to-page": "Povezava do strani z opisom datoteke",
+ "multimediaviewer-link-to-file": "Povezava do izvirne datoteke",
+ "multimediaviewer-share-explanation": "Povezavo kopirajte in jo prosto delite",
+ "multimediaviewer-embed-wt": "Wikibesedilo",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-embed-explanation": "Uporabite to kodo za vdelavo datoteke",
+ "multimediaviewer-html-embed-credit-link-text": "Povezava",
+ "multimediaviewer-default-embed-dimensions": "Privzeta velikost sličice",
+ "multimediaviewer-original-embed-dimensions": "Izvirna datoteka $1",
+ "multimediaviewer-large-embed-dimensions": "Velika $1",
+ "multimediaviewer-medium-embed-dimensions": "Srednja $1",
+ "multimediaviewer-small-embed-dimensions": "Majhna $1",
+ "multimediaviewer-description-page-button-text": "Več podrobnosti o datoteki",
+ "multimediaviewer-description-page-popup-text": "Več podrobnosti o datoteki na $1"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/sq.json b/www/wiki/extensions/MultimediaViewer/i18n/sq.json
new file mode 100644
index 00000000..9d9835ff
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/sq.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "GretaDoci"
+ ]
+ },
+ "multimediaviewer-optin-mmv": "Aktivizo Median Pamore"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/sr-ec.json b/www/wiki/extensions/MultimediaViewer/i18n/sr-ec.json
new file mode 100644
index 00000000..c1f9b0dc
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/sr-ec.json
@@ -0,0 +1,111 @@
+{
+ "@metadata": {
+ "authors": [
+ "Milicevic01",
+ "Сербијана",
+ "Obsuser",
+ "Acamicamacaraca",
+ "BadDog"
+ ]
+ },
+ "multimediaviewer-optin-pref": "Омогући <span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About прегледач датотека]</span>",
+ "multimediaviewer-file-page": "Иди на одговарајућу страницу датотеке",
+ "multimediaviewer-repository-local": "Више детаља",
+ "multimediaviewer-datetime-created": "Направљено: $1",
+ "multimediaviewer-datetime-uploaded": "Послато: $1",
+ "multimediaviewer-credit-fallback": "Погледај информације о аутору",
+ "multimediaviewer-thumbnail-error": "Жао нам је, датотеку није могуће приказати",
+ "multimediaviewer-thumbnail-error-description": "Изгледа да постоји технички проблем. Можете $1 или $3 ако се настави. Грешка: $2",
+ "multimediaviewer-thumbnail-error-retry": "пробати опет",
+ "multimediaviewer-thumbnail-error-report": "пријавити проблем",
+ "multimediaviewer-license-cc-pd": "Јавно власништво",
+ "multimediaviewer-license-pd": "Јавно власништво",
+ "multimediaviewer-license-default": "Види лиценцу",
+ "multimediaviewer-permission-title": "Детаљи дозволе",
+ "multimediaviewer-permission-link": "види услове",
+ "multimediaviewer-permission-link-hide": "сакриј услове",
+ "multimediaviewer-permission-viewmore": "Прикажи још",
+ "multimediaviewer-restriction-communist": "Ова слика садржи комунистичка обележја која су забрањена у неким земљама.",
+ "multimediaviewer-restriction-insignia": "Ова слика садржи званично обележје, њихова употреба подлеже ограничењима.",
+ "multimediaviewer-restriction-nazi": "Ова слика садржи нацистичка обележја која су забрањена у неким земљама.",
+ "multimediaviewer-restriction-trademarked": "Ова слика можда садржи регистровани заштитни знак.",
+ "multimediaviewer-restriction-default": "Ова слика можда подлеже неким другим ограничењима независно од статуса ауторских права. Погледајте страницу датотеке за више информација.",
+ "multimediaviewer-restriction-default-and-others": "Ова слика можда подлеже неким другим ограничењима независно од статуса ауторских права. Погледајте страницу датотеке за више информација.",
+ "multimediaviewer-about-mmv": "О",
+ "multimediaviewer-discuss-mmv": "Расправа",
+ "multimediaviewer-help-mmv": "Помоћ",
+ "multimediaviewer-optout-mmv": "Онемогући Media Viewer",
+ "multimediaviewer-optin-mmv": "Омогући прегледач датотека",
+ "multimediaviewer-optout-pending-mmv": "Онемогућавање Media Viewer-а",
+ "multimediaviewer-optin-pending-mmv": "Омогућавање Media Viewer-а",
+ "multimediaviewer-optout-help": "Media Viewer неће више бити коришћен за приказ слика. Да га користите опет кликните на „{{int:multimediaviewer-view-expanded}}“, па на „{{int:multimediaviewer-optin-mmv}}“.",
+ "multimediaviewer-optin-help": "Media Viewer ће бити коришћен за приказивање слика.",
+ "multimediaviewer-geolocation": "Локација: $1",
+ "multimediaviewer-reuse-link": "Користи ову датотеку",
+ "multimediaviewer-reuse-loading-placeholder": "Учитавам...",
+ "multimediaviewer-share-tab": "Дели",
+ "multimediaviewer-embed-tab": "Угради",
+ "multimediaviewer-download-link": "Преузми",
+ "multimediaviewer-download-preview-link-title": "Види у веб-прегледачу",
+ "multimediaviewer-download-original-button-name": "Преузми оригиналну датотеку",
+ "multimediaviewer-download-small-button-name": "Преузми у малој величини",
+ "multimediaviewer-download-medium-button-name": "Преузми у средњој величини",
+ "multimediaviewer-download-large-button-name": "Преузми велику",
+ "multimediaviewer-link-to-page": "Веза до одговарајуће странице датотеке",
+ "multimediaviewer-link-to-file": "Веза до оригиналне датотеке",
+ "multimediaviewer-share-explanation": "Копирајте и слободно делите ову везу",
+ "multimediaviewer-embed-wt": "Викитекст",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-embed-explanation": "Користите овај код да уградите датотеку",
+ "multimediaviewer-text-embed-credit-text-bl": "Аутор: $1, $2, $3",
+ "multimediaviewer-text-embed-credit-text-b": "Аутор: $1, $2",
+ "multimediaviewer-html-embed-credit-text-bl": "Аутор: $1, $2, $3",
+ "multimediaviewer-html-embed-credit-text-b": "Аутор: $1, $2",
+ "multimediaviewer-html-embed-credit-link-text": "Веза",
+ "multimediaviewer-embed-byline": "Аутор: $1",
+ "multimediaviewer-embed-license": "Под лиценцом $1.",
+ "multimediaviewer-embed-via": "Преко $1.",
+ "multimediaviewer-default-embed-dimensions": "Подразумевана величина",
+ "multimediaviewer-original-embed-dimensions": "Оригинална датотека $1",
+ "multimediaviewer-large-embed-dimensions": "Велика $1",
+ "multimediaviewer-medium-embed-dimensions": "Средња $1",
+ "multimediaviewer-small-embed-dimensions": "Мала $1",
+ "multimediaviewer-embed-dimensions": "$1 × $2 п",
+ "multimediaviewer-description-page-button-text": "Прикажи детаље",
+ "multimediaviewer-description-page-popup-text": "Прикажи детаље са пројекта $1",
+ "multimediaviewer-commons-subtitle": "Ризница слободних медијских датотека",
+ "multimediaviewer-view-expanded": "Рашири",
+ "multimediaviewer-view-config": "Подешавања",
+ "multimediaviewer-close-popup-text": "Затвори ову алатку (Esc)",
+ "multimediaviewer-fullscreen-popup-text": "Прикажи преко целог екрана",
+ "multimediaviewer-defullscreen-popup-text": "Изађи из целог екрана",
+ "multimediaviewer-next-image-alt-text": "Следећа слика",
+ "multimediaviewer-prev-image-alt-text": "Претходна слика",
+ "multimediaviewer-title-popup-text": "Опис",
+ "multimediaviewer-credit-popup-text": "Аутор и информације о извору",
+ "multimediaviewer-title-popup-text-more": "Види цео опис",
+ "multimediaviewer-download-attribution-cta-header": "Морате навести аутора",
+ "multimediaviewer-download-optional-attribution-cta-header": "Можете навести аутора",
+ "multimediaviewer-download-attribution-cta": "Покажи ми како",
+ "multimediaviewer-reuse-warning-nonfree": "Ова датотека није под слободном лиценцом.",
+ "multimediaviewer-reuse-warning-noattribution": "Ова датотека нема информације о ауторству.",
+ "multimediaviewer-reuse-warning-generic": "Проверите [$1 њене детаље] пре коришћења.",
+ "multimediaviewer-attr-plain": "Текст",
+ "multimediaviewer-options-tooltip": "Омогућите или онемогућите прегледач датотека",
+ "multimediaviewer-options-dialog-header": "Онемогући Media Viewer?",
+ "multimediaviewer-options-text-header": "Онемогући ову могућност за све датотеке.",
+ "multimediaviewer-options-text-body": "Накнадно је можете омогућити на страници саме датотеке.",
+ "multimediaviewer-options-learn-more": "Сазнајте више",
+ "multimediaviewer-option-submit-button": "Онемогући Media Viewer",
+ "multimediaviewer-option-cancel-button": "Откажи",
+ "multimediaviewer-disable-confirmation-header": "Онемогућили сте Media Viewer",
+ "multimediaviewer-disable-confirmation-text": "Следећи пут када кликнете на неку слику на $1, биће вам приказани сви детаљи дотичне датотеке.",
+ "multimediaviewer-enable-dialog-header": "Омогући прегледач датотека?",
+ "multimediaviewer-enable-text-header": "Омогући ову могућност као подразумевану за све датотеке.",
+ "multimediaviewer-enable-submit-button": "Омогући прегледач датотека",
+ "multimediaviewer-enable-confirmation-header": "Омогућили сте Media Viewer за све датотеке",
+ "multimediaviewer-enable-confirmation-text": "Следећи пут када кликнете на неку слику на $1, Media Viewer ће је приказати.",
+ "multimediaviewer-enable-alert": "Media Viewer је тренутно онемогућен",
+ "multimediaviewer-disable-info-title": "Онемогућили сте Media Viewer",
+ "multimediaviewer-disable-info": "И даље можете користити Media Viewer за појединачне датотеке."
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/sr-el.json b/www/wiki/extensions/MultimediaViewer/i18n/sr-el.json
new file mode 100644
index 00000000..f46f2813
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/sr-el.json
@@ -0,0 +1,93 @@
+{
+ "@metadata": {
+ "authors": [
+ "Milicevic01",
+ "Obsuser"
+ ]
+ },
+ "multimediaviewer-optin-pref": "Omogući <span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About Media Viewer]</span>",
+ "multimediaviewer-file-page": "Idi na odgovarajuću stranicu datoteke",
+ "multimediaviewer-repository-local": "Saznajte više",
+ "multimediaviewer-datetime-created": "Napravljeno: $1",
+ "multimediaviewer-datetime-uploaded": "Poslato: $1",
+ "multimediaviewer-credit-fallback": "Vidi informacije o autoru",
+ "multimediaviewer-thumbnail-error": "Žao nam je, datoteku nije moguće prikazati",
+ "multimediaviewer-thumbnail-error-description": "Izgleda da postoji tehnički problem. Možete $1 ili $3 ako se nastavi. Greška: $2",
+ "multimediaviewer-thumbnail-error-retry": "probaj opet",
+ "multimediaviewer-license-cc-pd": "Javno vlasništvo",
+ "multimediaviewer-license-pd": "Javno vlasništvo",
+ "multimediaviewer-license-default": "Vidi licencu",
+ "multimediaviewer-permission-title": "Detalji dozvole",
+ "multimediaviewer-permission-link": "vidi uslove",
+ "multimediaviewer-permission-link-hide": "sakrij uslove",
+ "multimediaviewer-permission-viewmore": "Prikaži još",
+ "multimediaviewer-restriction-communist": "Ova slika sadrži komunistička obeležja koja su zabranjena u nekim zemljama.",
+ "multimediaviewer-restriction-insignia": "Ova slika sadrži zvanično obeležje, njihova upotreba podleže ograničenjima.",
+ "multimediaviewer-restriction-nazi": "Ova slika sadrži nacistička obeležja koja su zabranjena u nekim zemljama.",
+ "multimediaviewer-restriction-trademarked": "Ova slika možda sadrži registrovani zaštitni znak.",
+ "multimediaviewer-restriction-default": "Ova slika možda podleže nekim drugim ograničenjima nezavisno od statusa autorskih prava. Pogledajte stranicu datoteke za više informacija.",
+ "multimediaviewer-restriction-default-and-others": "Ova slika možda podleže nekim drugim ograničenjima nezavisno od statusa autorskih prava. Pogledajte stranicu datoteke za više informacija.",
+ "multimediaviewer-about-mmv": "O Media Viewer-u",
+ "multimediaviewer-discuss-mmv": "Rasprava",
+ "multimediaviewer-help-mmv": "Pomoć",
+ "multimediaviewer-optout-mmv": "Onemogući Media Viewer",
+ "multimediaviewer-optin-mmv": "Omogući Media Viewer",
+ "multimediaviewer-optout-pending-mmv": "Onemogućavanje Media Viewer-a",
+ "multimediaviewer-optin-pending-mmv": "Omogućavanje Media Viewer-a",
+ "multimediaviewer-optout-help": "Media Viewer neće više biti korišćen za prikaz slika. Da ga koristite opet kliknite na „{{int:multimediaviewer-view-expanded}}“, pa na „{{int:multimediaviewer-optin-mmv}}“.",
+ "multimediaviewer-optin-help": "Media Viewer će biti korišćen za prikazivanje slika.",
+ "multimediaviewer-geolocation": "Lokacija: $1",
+ "multimediaviewer-reuse-link": "Koristi ovu datoteku",
+ "multimediaviewer-reuse-loading-placeholder": "Učitavanje...",
+ "multimediaviewer-share-tab": "Deli",
+ "multimediaviewer-embed-tab": "Ugradi",
+ "multimediaviewer-download-link": "Preuzmi",
+ "multimediaviewer-download-preview-link-title": "Vidi u veb-pregledaču",
+ "multimediaviewer-download-original-button-name": "Preuzmi originalnu datoteku",
+ "multimediaviewer-download-small-button-name": "Preuzmi u maloj veličini",
+ "multimediaviewer-download-medium-button-name": "Preuzmi u srednjoj veličini",
+ "multimediaviewer-download-large-button-name": "Preuzmi veliku",
+ "multimediaviewer-link-to-page": "Veza do odgovarajuće stranice datoteke",
+ "multimediaviewer-link-to-file": "Veza do originalne datoteke",
+ "multimediaviewer-share-explanation": "Kopirajte i slobodno delite ovu vezu",
+ "multimediaviewer-embed-wt": "Vikitekst",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-embed-explanation": "Koristite ovaj kod da ugradite datoteku",
+ "multimediaviewer-default-embed-dimensions": "Podrazumevana veličina",
+ "multimediaviewer-original-embed-dimensions": "Originalna datoteka $1",
+ "multimediaviewer-large-embed-dimensions": "Velika $1",
+ "multimediaviewer-medium-embed-dimensions": "Srednja $1",
+ "multimediaviewer-small-embed-dimensions": "Mala $1",
+ "multimediaviewer-description-page-button-text": "Prikaži detalje",
+ "multimediaviewer-description-page-popup-text": "Prikaži detalje na $1",
+ "multimediaviewer-commons-subtitle": "Riznica slobodnih medijskih datoteka",
+ "multimediaviewer-view-expanded": "Raširi",
+ "multimediaviewer-view-config": "Podešavanja",
+ "multimediaviewer-close-popup-text": "Zatvori ovu alatku (Esc)",
+ "multimediaviewer-fullscreen-popup-text": "Prikaži preko celog ekrana",
+ "multimediaviewer-defullscreen-popup-text": "Izađi iz celog ekrana",
+ "multimediaviewer-title-popup-text": "Naziv datoteke",
+ "multimediaviewer-credit-popup-text": "Autor i informacije o izvoru",
+ "multimediaviewer-title-popup-text-more": "Vidi ceo opis",
+ "multimediaviewer-download-attribution-cta-header": "Morate navesti autora",
+ "multimediaviewer-download-optional-attribution-cta-header": "Možete navesti autora",
+ "multimediaviewer-download-attribution-cta": "Pokaži mi kako",
+ "multimediaviewer-attr-plain": "Tekst",
+ "multimediaviewer-options-tooltip": "Omogući ili onemogući Media Viewer",
+ "multimediaviewer-options-dialog-header": "Onemogući Media Viewer?",
+ "multimediaviewer-options-text-header": "Onemogući ovu mogućnost za sve datoteke.",
+ "multimediaviewer-options-text-body": "Naknadno je možete omogućiti na stranici same datoteke.",
+ "multimediaviewer-options-learn-more": "Saznajte više",
+ "multimediaviewer-option-submit-button": "Onemogući Media Viewer",
+ "multimediaviewer-option-cancel-button": "Otkaži",
+ "multimediaviewer-disable-confirmation-header": "Onemogućili ste Media Viewer",
+ "multimediaviewer-disable-confirmation-text": "Sledeći put kada kliknete na neku sliku na $1, biće vam prikazani svi detalji dotične datoteke.",
+ "multimediaviewer-enable-dialog-header": "Omogući Media Viewer?",
+ "multimediaviewer-enable-text-header": "Omogući ovu mogućnost kao podrazumevanu za sve datoteke.",
+ "multimediaviewer-enable-submit-button": "Omogući Media Viewer",
+ "multimediaviewer-enable-confirmation-header": "Omogućili ste Media Viewer za sve datoteke",
+ "multimediaviewer-enable-confirmation-text": "Sledeći put kada kliknete na neku sliku na $1, Media Viewer će je prikazati.",
+ "multimediaviewer-enable-alert": "Media Viewer je trenutno onemogućen",
+ "multimediaviewer-disable-info-title": "Onemogućili ste Media Viewer",
+ "multimediaviewer-disable-info": "I dalje možete koristiti Media Viewer za pojedinačne datoteke."
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/su.json b/www/wiki/extensions/MultimediaViewer/i18n/su.json
new file mode 100644
index 00000000..0aec1152
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/su.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Uchup19"
+ ]
+ },
+ "multimediaviewer-option-cancel-button": "Bolay"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/sv.json b/www/wiki/extensions/MultimediaViewer/i18n/sv.json
new file mode 100644
index 00000000..f920f8e3
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/sv.json
@@ -0,0 +1,119 @@
+{
+ "@metadata": {
+ "authors": [
+ "Ainali",
+ "Jopparn",
+ "Lokal Profil",
+ "NH",
+ "WikiPhoenix",
+ "Stens51",
+ "Abbedabb",
+ "Warrakkk",
+ "Hangsna",
+ "Bengtsson96"
+ ]
+ },
+ "multimediaviewer-desc": "Expandera miniatyrer i en större storlek i fulkskärmsgränssnitt.",
+ "multimediaviewer-pref": "Mediavisare",
+ "multimediaviewer-pref-desc": "Förbättra din multimediaupplevelse med detta nya verktyg. Det visar bilder i större storlek på sidor som har miniatyrer. Bilder visas i ett trevligare fullskärmsöverlägg, och kan också ses i full storlek.",
+ "multimediaviewer-optin-pref": "Aktivera <span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About Mediavisaren]</span>",
+ "multimediaviewer-file-page": "Gå till motsvarande filsida",
+ "multimediaviewer-repository-local": "Mer detaljer",
+ "multimediaviewer-datetime-created": "Skapades: $1",
+ "multimediaviewer-datetime-uploaded": "Laddades upp: $1",
+ "multimediaviewer-credit-fallback": "Visa information om skapare",
+ "multimediaviewer-multiple-authors": "{{PLURAL:$1|ytterligare en skapare|ytterligare $1 skapare}}",
+ "multimediaviewer-multiple-authors-combine": "$1 och $2",
+ "multimediaviewer-metadata-error": "Kunde inte läsa in bilddetaljer. (Fel: $1)",
+ "multimediaviewer-thumbnail-error": "Tyvärr, filen kan inte visas",
+ "multimediaviewer-thumbnail-error-description": "Ett tekniskt fel har uppstått. Du kan $1 eller $3 om det kvarstår. Felmeddelande: $2",
+ "multimediaviewer-thumbnail-error-retry": "försöka igen",
+ "multimediaviewer-thumbnail-error-report": "rapportera problemet",
+ "multimediaviewer-license-cc-by-4.0": "CC BY 4.0",
+ "multimediaviewer-license-cc-by-sa-4.0": "CC BY-SA 4.0",
+ "multimediaviewer-license-cc-pd": "Public Domain",
+ "multimediaviewer-license-pd": "Public Domain",
+ "multimediaviewer-license-default": "Visa licens",
+ "multimediaviewer-permission-title": "Behörighetsdetaljer",
+ "multimediaviewer-permission-link": "visa villkor",
+ "multimediaviewer-permission-link-hide": "dölj villkor",
+ "multimediaviewer-permission-viewmore": "Visa mer",
+ "multimediaviewer-restriction-trademarked": "Denna bild innehåller material som kan vara föremål för varumärkesrättsliga lagar.",
+ "multimediaviewer-about-mmv": "Om",
+ "multimediaviewer-discuss-mmv": "Diskussion",
+ "multimediaviewer-help-mmv": "Hjälp",
+ "multimediaviewer-optout-mmv": "Inaktivera Mediavisaren",
+ "multimediaviewer-optin-mmv": "Aktivera Mediavisaren",
+ "multimediaviewer-optout-pending-mmv": "Inaktivera Mediavisaren",
+ "multimediaviewer-optin-pending-mmv": "Aktivera Mediavisaren",
+ "multimediaviewer-optout-help": "Mediavisaren kommer inte längre att användas för att visa bilder. För att använda den igen, klicka på \"{{int:multimediaviewer-view-expanded}}\"-knappen bredvid valfri bild. Klicka sedan på \"{{int:multimediaviewer-optin-mmv}}\".",
+ "multimediaviewer-optin-help": "Mediavisaren kommer att användas för att visa bilder.",
+ "multimediaviewer-geolocation": "Plats: $1",
+ "multimediaviewer-reuse-link": "Använd eller bädda in denna fil",
+ "multimediaviewer-reuse-loading-placeholder": "Läser in...",
+ "multimediaviewer-share-tab": "Dela",
+ "multimediaviewer-embed-tab": "Bädda in",
+ "multimediaviewer-download-link": "Ladda ner denna fil",
+ "multimediaviewer-download-preview-link-title": "Visa i webbläsare",
+ "multimediaviewer-download-original-button-name": "Ladda ned originalfil",
+ "multimediaviewer-download-small-button-name": "Ladda ner liten storlek",
+ "multimediaviewer-download-medium-button-name": "Ladda ner medium storlek",
+ "multimediaviewer-download-large-button-name": "Ladda ner stor storlek",
+ "multimediaviewer-link-to-page": "Länk till filbeskrivningssidan",
+ "multimediaviewer-link-to-file": "Länk till originalfil",
+ "multimediaviewer-share-explanation": "Kopiera och dela fritt länken",
+ "multimediaviewer-embed-wt": "Wikitext",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-embed-explanation": "Använd denna kod för att bädda in filen",
+ "multimediaviewer-text-embed-credit-text-bl": "Av $1, $2, $3",
+ "multimediaviewer-text-embed-credit-text-b": "Av $1, $2",
+ "multimediaviewer-html-embed-credit-text-bl": "Av $1, $2, $3",
+ "multimediaviewer-html-embed-credit-text-b": "Av $1, $2",
+ "multimediaviewer-html-embed-credit-link-text": "Länk",
+ "multimediaviewer-embed-byline": "Av $1",
+ "multimediaviewer-embed-license": "Licenserat under $1.",
+ "multimediaviewer-embed-via": "Via $1.",
+ "multimediaviewer-default-embed-dimensions": "Standardstorlek för miniatyrer",
+ "multimediaviewer-original-embed-dimensions": "Originalfil $1",
+ "multimediaviewer-large-embed-dimensions": "Stor $1",
+ "multimediaviewer-medium-embed-dimensions": "Medium $1",
+ "multimediaviewer-small-embed-dimensions": "Liten $1",
+ "multimediaviewer-description-page-button-text": "Fler detaljer om denna fil",
+ "multimediaviewer-description-page-popup-text": "Mer detaljer om den här filen på $1",
+ "multimediaviewer-commons-subtitle": "Det fria mediearkivet",
+ "multimediaviewer-view-expanded": "Öppna i Mediavisare",
+ "multimediaviewer-view-config": "Konfigurering",
+ "multimediaviewer-close-popup-text": "Stäng detta verktyg (Esc)",
+ "multimediaviewer-fullscreen-popup-text": "Visa i helskärmsläge",
+ "multimediaviewer-defullscreen-popup-text": "Lämnar helskärmsläge",
+ "multimediaviewer-next-image-alt-text": "Visa nästa bild",
+ "multimediaviewer-prev-image-alt-text": "Visa föregående bild",
+ "multimediaviewer-title-popup-text": "Beskrivning",
+ "multimediaviewer-credit-popup-text": "Information om skapare och källa",
+ "multimediaviewer-title-popup-text-more": "Visa full beskrivning",
+ "multimediaviewer-credit-popup-text-more": "Visa all information om skapare och källa",
+ "multimediaviewer-download-attribution-cta-header": "Du måste ge skaparen erkännande",
+ "multimediaviewer-download-optional-attribution-cta-header": "Du kan ge skaparen erkännande",
+ "multimediaviewer-download-attribution-cta": "Visa mig hur",
+ "multimediaviewer-reuse-warning-deletion": "Filen är föreslagen för radering.",
+ "multimediaviewer-reuse-warning-nonfree": "Filen har inte en fri licens.",
+ "multimediaviewer-reuse-warning-generic": "Kontrollera [$1 filens detaljer] före användning.",
+ "multimediaviewer-attr-plain": "Klartext",
+ "multimediaviewer-options-tooltip": "Aktivera eller inaktivera Mediavisaren",
+ "multimediaviewer-options-dialog-header": "Inaktivera Mediavisaren?",
+ "multimediaviewer-options-text-header": "Skippa denna visningsfunktion för alla filer.",
+ "multimediaviewer-options-text-body": "Du kan aktivera det senare genom filens beskrivningssida.",
+ "multimediaviewer-options-learn-more": "Läs mer",
+ "multimediaviewer-option-submit-button": "Inaktivera Mediavisaren",
+ "multimediaviewer-option-cancel-button": "Avbryt",
+ "multimediaviewer-disable-confirmation-header": "Du har inaktiverat Media Viewer",
+ "multimediaviewer-disable-confirmation-text": "Nästa gång du klickar på en miniatyrbild på $1 kommer du direkt se all filinformation.",
+ "multimediaviewer-enable-dialog-header": "Aktiverar Media Viewer?",
+ "multimediaviewer-enable-text-header": "Aktivera denna mediavisare för alla filer som standard.",
+ "multimediaviewer-enable-submit-button": "Aktivera Media Viewer",
+ "multimediaviewer-enable-confirmation-header": "Du har aktiverat Media Viewer för alla filer",
+ "multimediaviewer-enable-confirmation-text": "Nästa gång du klickar på en miniatyrbild på $1 kommer Media Viewer att användas.",
+ "multimediaviewer-enable-alert": "Media Viewer är nu inaktiverad",
+ "multimediaviewer-disable-info-title": "Du har inaktiverat Media Viewer",
+ "multimediaviewer-disable-info": "Du kan fortfarande visa enskilda filer med Media Viewer."
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/sw.json b/www/wiki/extensions/MultimediaViewer/i18n/sw.json
new file mode 100644
index 00000000..c6b1086f
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/sw.json
@@ -0,0 +1,14 @@
+{
+ "@metadata": {
+ "authors": [
+ "Lloffiwr"
+ ]
+ },
+ "multimediaviewer-license-default": "Tazama leseni",
+ "multimediaviewer-permission-title": "Maelezo ya leseni",
+ "multimediaviewer-permission-viewmore": "Tazama zaidi",
+ "multimediaviewer-about-mmv": "Kuhusu Media Viewer",
+ "multimediaviewer-help-mmv": "Msaada",
+ "multimediaviewer-reuse-link": "Tumia faili hili",
+ "multimediaviewer-embed-html": "HTML"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/ta.json b/www/wiki/extensions/MultimediaViewer/i18n/ta.json
new file mode 100644
index 00000000..98131e21
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/ta.json
@@ -0,0 +1,12 @@
+{
+ "@metadata": {
+ "authors": [
+ "ElangoRamanujam",
+ "Kalyanasundar"
+ ]
+ },
+ "multimediaviewer-optin-pref": "இயலச்செய்",
+ "multimediaviewer-repository-local": "மேலதிக விபரங்கள்",
+ "multimediaviewer-download-link": "இக்கோப்பைப் பதிவிறக்குக",
+ "multimediaviewer-description-page-button-text": "மேலதிக விபரங்கள்"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/tcy.json b/www/wiki/extensions/MultimediaViewer/i18n/tcy.json
new file mode 100644
index 00000000..de5705b3
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/tcy.json
@@ -0,0 +1,9 @@
+{
+ "@metadata": {
+ "authors": [
+ "Soundarya shetty s",
+ "Bharathesha Alasandemajalu"
+ ]
+ },
+ "multimediaviewer-thumbnail-error-report": "ಸಮಸ್ಯೆನ್ ವರದಿ ಮಲ್ಪುಲೆ"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/te.json b/www/wiki/extensions/MultimediaViewer/i18n/te.json
new file mode 100644
index 00000000..219b4b9c
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/te.json
@@ -0,0 +1,60 @@
+{
+ "@metadata": {
+ "authors": [
+ "Chaduvari",
+ "Ravichandra",
+ "రహ్మానుద్దీన్",
+ "Veeven"
+ ]
+ },
+ "multimediaviewer-desc": "పూర్తి స్క్రీను అంతరవర్తినిలో గోరంతబొమ్మను పెద్ద పరిణామంలోకి మార్చు.",
+ "multimediaviewer-pref": "మీడియా వ్యూయర్",
+ "multimediaviewer-pref-desc": "ఈ సరికొత్త ఉపకరణంతో బొమ్మలను చూసే అనుభవాన్ని మరింత మెరుగుపరుచుకోండి. ఇది పేజీలో ఉన్న గోరంతబొమ్మ(నఖచిత్రా)లను పెద్దదిగా చూపిస్తుంది. బొమ్మలు మెరుగైన పూర్తి స్క్రీన్ ఓవర్లే లో చూపబడతాయి, పూర్తి స్థాయి బొమ్మ పరిమాణం కూడా చూడవచ్చు.",
+ "multimediaviewer-optin-pref": "సరికొత్త బొమ్మలను చూసే అనుభూతిని సచేతనం చేసుకోండి",
+ "multimediaviewer-file-page": "సంబంధిత దస్త్రపు పేజీకు వెళ్ళండి",
+ "multimediaviewer-repository-local": "మరిన్ని వివరాలు",
+ "multimediaviewer-datetime-created": "$1 న సృష్టించబడింది",
+ "multimediaviewer-datetime-uploaded": "$1 న ఎక్కించబడింది",
+ "multimediaviewer-metadata-error": "దోషం : $1. బొమ్మ సమాచారాన్ని చూపలేకపోతున్నాం.",
+ "multimediaviewer-thumbnail-error": "దోషం : $1. గోరంతబొమ్మ (నఖచిత్రం) సమాచారాన్ని చూపించలేకపోతున్నాం.",
+ "multimediaviewer-license-cc-pd": "సార్వజనీనం",
+ "multimediaviewer-license-pd": "సార్వజనీనం",
+ "multimediaviewer-license-default": "లైసెన్స్ వివరాలు చూడండి",
+ "multimediaviewer-permission-title": "అనుమతి వివరాలు",
+ "multimediaviewer-permission-link": "నిబంధనలు చూడండి",
+ "multimediaviewer-permission-viewmore": "మరింత చూడండి",
+ "multimediaviewer-about-mmv": "గురించి",
+ "multimediaviewer-discuss-mmv": "చర్చ",
+ "multimediaviewer-help-mmv": "సహాయం",
+ "multimediaviewer-geolocation": "స్థానం : $1",
+ "multimediaviewer-reuse-link": "ఈ దస్త్రాన్ని వాడండి",
+ "multimediaviewer-reuse-loading-placeholder": "లోడవుతోంది...",
+ "multimediaviewer-share-tab": "పంచుకోండి",
+ "multimediaviewer-embed-tab": "ఇముడ్చు",
+ "multimediaviewer-download-link": "దింపుకోండి",
+ "multimediaviewer-download-preview-link-title": "విహారిణిలో మునుజూపు చూడండి",
+ "multimediaviewer-download-original-button-name": "అసలు పరిమాణంలో దింపుకోండి",
+ "multimediaviewer-download-small-button-name": "చిన్ని పరిమాణంలో దింపుకోండి",
+ "multimediaviewer-download-medium-button-name": "మధ్యస్థ పరిమాణంలో దింపుకోండి",
+ "multimediaviewer-download-large-button-name": "పెద్ద పరిమాణంలో దింపుకోండి",
+ "multimediaviewer-link-to-page": "దస్త్రపు వివరణ పేజీకి లంకె",
+ "multimediaviewer-link-to-file": "అసలు దస్త్రానికి లంకె",
+ "multimediaviewer-share-explanation": "నకలు తీసుకొని, స్వేచ్ఛగా లంకెను పంచుకోండి",
+ "multimediaviewer-embed-wt": "వికీపాఠ్యం",
+ "multimediaviewer-embed-html": "హెచ్.టి.ఎం.ఎల్",
+ "multimediaviewer-embed-explanation": "దస్త్రాన్ని ఇమిడ్చేందుకు ఈ కోడ్ ను వాడండి",
+ "multimediaviewer-embed-byline": "$1 ద్వారా",
+ "multimediaviewer-embed-license": "$1 లైసెన్సుకు లోబడి",
+ "multimediaviewer-embed-via": "$1 ద్వారా.",
+ "multimediaviewer-default-embed-dimensions": "అప్రమేయ నఖచిత్ర(గోరంతబొమ్మ) పరిమాణం",
+ "multimediaviewer-original-embed-dimensions": "అసలు పరిమాణం $1",
+ "multimediaviewer-large-embed-dimensions": "పెద్ద పరిమాణం $1",
+ "multimediaviewer-medium-embed-dimensions": "మధ్యస్థ పరిమాణం $1",
+ "multimediaviewer-small-embed-dimensions": "చిన్న పరిమాణం $1",
+ "multimediaviewer-description-page-button-text": "ఈ దస్త్రం గురించి మరిన్ని వివరాలు",
+ "multimediaviewer-description-page-popup-text": "$1 పై మరిన్ని వివరాలు",
+ "multimediaviewer-commons-subtitle": "స్వేచ్ఛా దస్త్రాల భాండాగారం",
+ "multimediaviewer-view-expanded": "వీక్షణాన్ని పెద్దది చేయండి",
+ "multimediaviewer-next-image-alt-text": "తర్వాతి బొమ్మను చూపించు",
+ "multimediaviewer-prev-image-alt-text": "మునుపటి బొమ్మను చూపించు"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/tg-cyrl.json b/www/wiki/extensions/MultimediaViewer/i18n/tg-cyrl.json
new file mode 100644
index 00000000..ad5aaeb5
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/tg-cyrl.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "ToJack"
+ ]
+ },
+ "multimediaviewer-embed-wt": "Вики-матн"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/th.json b/www/wiki/extensions/MultimediaViewer/i18n/th.json
new file mode 100644
index 00000000..ef85e31f
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/th.json
@@ -0,0 +1,27 @@
+{
+ "@metadata": {
+ "authors": [
+ "Horus",
+ "Nullzero",
+ "Taweetham",
+ "Ans"
+ ]
+ },
+ "multimediaviewer-desc": "ขยายรูปขนาดย่อให้มีขนาดใหญ่ขึ้นในอินเตอร์เฟซเต็มหน้าจอ",
+ "multimediaviewer-pref": "Media Viewer",
+ "multimediaviewer-pref-desc": "ด้วยเครื่องมือใหม่นี้ คุณสามารถดูสื่อได้ดีไปกว่าเดิม เครื่องมือจะแสดงภาพขนาดใหญ่ขึ้นบนหน้าที่มีรูปขนาดย่อ คุณยังสามารถดูภาพแบบในอินเตอร์เฟซเต็มหน้าจอและดูภาพในขนาดเต็มได้ด้วย",
+ "multimediaviewer-file-page": "ไปยังหน้าไฟล์ที่ตรงกัน",
+ "multimediaviewer-repository-local": "ดูรายละเอียดเพิ่มเติมเกี่ยวกับไฟล์นี้",
+ "multimediaviewer-datetime-created": "สร้าง: $1",
+ "multimediaviewer-datetime-uploaded": "อัปโหลด: $1",
+ "multimediaviewer-license-cc-pd": "สาธารณสมบัติ",
+ "multimediaviewer-license-pd": "สาธารณสมบัติ",
+ "multimediaviewer-license-default": "ดูสัญญาอนุญาต",
+ "multimediaviewer-permission-title": "รายละเอียดสัญญาอนุญาต",
+ "multimediaviewer-permission-link": "ดูเงื่อนไข",
+ "multimediaviewer-permission-viewmore": "ดูเพิ่มเติม",
+ "multimediaviewer-about-mmv": "เกี่ยวกับ Media Viewer",
+ "multimediaviewer-discuss-mmv": "อธิบายคุณลักษณะนี้",
+ "multimediaviewer-geolocation": "สถานที่: $1",
+ "multimediaviewer-reuse-link": "ใช้ไฟล์นี้"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/tl.json b/www/wiki/extensions/MultimediaViewer/i18n/tl.json
new file mode 100644
index 00000000..459fa3ea
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/tl.json
@@ -0,0 +1,12 @@
+{
+ "@metadata": {
+ "authors": [
+ "Jewel457",
+ "Leeheonjin"
+ ]
+ },
+ "multimediaviewer-repository-local": "Iba pang detalye tungkol sa file",
+ "multimediaviewer-thumbnail-error-report": "iulat ang palahatla (issue) na ito",
+ "multimediaviewer-optin-mmv": "Pinapagana ang Media Viewer",
+ "multimediaviewer-optout-pending-mmv": "Isinasara ang Media Viewer"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/tr.json b/www/wiki/extensions/MultimediaViewer/i18n/tr.json
new file mode 100644
index 00000000..84c8ed66
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/tr.json
@@ -0,0 +1,80 @@
+{
+ "@metadata": {
+ "authors": [
+ "Ceas08",
+ "Incelemeelemani",
+ "Rapsar",
+ "SiLveRLeaD",
+ "Sucsuzz",
+ "Meelo",
+ "Sayginer",
+ "Violetanka",
+ "McAang"
+ ]
+ },
+ "multimediaviewer-desc": "Küçük resimleri daha büyük boyutlara genişleterek, tam ekran arayüzde gösterir.",
+ "multimediaviewer-pref": "Ortam Görüntüleyici",
+ "multimediaviewer-pref-desc": "Bu yeni araçla multimedya görüntüleme deneyiminizi geliştirin. Bu sayede küçük sayfaları daha büyük boyutlarda görüntüleyebilirsiniz. Ayrıca görüntüler tam ekran arayüzle ve tam boyutlu görüntülebeilir.",
+ "multimediaviewer-optin-pref": "<span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About Ortam Görüntüleyici]</span>'yi etkinleştir",
+ "multimediaviewer-file-page": "İlgili dosya sayfasına git",
+ "multimediaviewer-repository-local": "Bu dosya hakkında daha fazla bilgi",
+ "multimediaviewer-datetime-created": "Oluşturulma tarihi: $1",
+ "multimediaviewer-datetime-uploaded": "Yüklenme tarihi: $1",
+ "multimediaviewer-metadata-error": "Hata: Resim verisi yüklenemedi. $1",
+ "multimediaviewer-thumbnail-error": "Hata: Küçük resim verisi yüklenemedi. $1",
+ "multimediaviewer-license-cc-pd": "Kamu Malı",
+ "multimediaviewer-license-pd": "Kamu Malı",
+ "multimediaviewer-license-default": "Lisansı göster",
+ "multimediaviewer-permission-title": "Lisans ayrıntıları",
+ "multimediaviewer-permission-link": "şartları görüntüle",
+ "multimediaviewer-permission-viewmore": "Daha fazla",
+ "multimediaviewer-about-mmv": "Ortam Görüntüleyici hakkında",
+ "multimediaviewer-discuss-mmv": "Bu özelliği tartış",
+ "multimediaviewer-help-mmv": "Yardım",
+ "multimediaviewer-optout-mmv": "Ortam Görüntüleyici'yi devre dışı bırak",
+ "multimediaviewer-optin-mmv": "Ortam Görüntüleyici'yi etkinleştir",
+ "multimediaviewer-optout-pending-mmv": "Ortam Görüntüleyici devre dışı bırakılıyor",
+ "multimediaviewer-optin-pending-mmv": "Ortam Görüntüleyici etkinleştiriliyor",
+ "multimediaviewer-optout-help": "Seçerseniz artık görüntüler için Ortam Görüntüleyici kullanılmayacak. Tekrar kullanmak için, herhangi bir görüntünün yanındaki \"{{int:multimediaviewer-view-expanded}}\" butonuna ve sonrasında \"{{int:multimediaviewer-optin-mmv}}\" seçeneğine tıklayın.",
+ "multimediaviewer-optin-help": "Artık görüntüler için Ortam Görüntüleyici kullanılacak.",
+ "multimediaviewer-geolocation": "Konum: $1",
+ "multimediaviewer-reuse-link": "Bu dosyayı paylaş veya bir siteye yerleştir",
+ "multimediaviewer-reuse-loading-placeholder": "Yükleniyor...",
+ "multimediaviewer-share-tab": "Paylaş",
+ "multimediaviewer-embed-tab": "Göm",
+ "multimediaviewer-download-link": "Bu dosyayı indirin",
+ "multimediaviewer-download-preview-link-title": "Tarayıcıda görüntüle",
+ "multimediaviewer-download-original-button-name": "Özgün dosyayı indir",
+ "multimediaviewer-download-small-button-name": "Küçük boyutta indir",
+ "multimediaviewer-download-medium-button-name": "Orta boyutta indir",
+ "multimediaviewer-download-large-button-name": "Büyük boyutta indir",
+ "multimediaviewer-link-to-page": "Dosya açıklama sayfası bağlantısı",
+ "multimediaviewer-link-to-file": "Özgün dosya bağlantısı",
+ "multimediaviewer-share-explanation": "Bağlantıyı kopyala ve özgürce paylaş",
+ "multimediaviewer-embed-wt": "Vikimetin",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-embed-explanation": "Dosyayı gömmek için bu kodu kullanın",
+ "multimediaviewer-embed-byline": "$1 tarafından",
+ "multimediaviewer-embed-license": "$1 ile lisanslanmıştır.",
+ "multimediaviewer-embed-via": "$1 kaynağındandır.",
+ "multimediaviewer-default-embed-dimensions": "Varsayılan küçük resim boyutu",
+ "multimediaviewer-original-embed-dimensions": "Özgün dosya $1",
+ "multimediaviewer-large-embed-dimensions": "Büyük $1",
+ "multimediaviewer-medium-embed-dimensions": "Orta $1",
+ "multimediaviewer-small-embed-dimensions": "Küçük $1",
+ "multimediaviewer-description-page-button-text": "Bu dosya hakkında daha fazla bilgi",
+ "multimediaviewer-description-page-popup-text": "$1 üzerinde bu dosya hakkında daha fazla bilgi",
+ "multimediaviewer-commons-subtitle": "Özgür ortam havuzu",
+ "multimediaviewer-view-expanded": "Medya Görüntüleyici'de açın",
+ "multimediaviewer-close-popup-text": "Aracı kapat (Esc)",
+ "multimediaviewer-fullscreen-popup-text": "Tam ekran göster",
+ "multimediaviewer-defullscreen-popup-text": "Tam ekrandan çık",
+ "multimediaviewer-title-popup-text": "Dosya adı",
+ "multimediaviewer-title-popup-text-more": "Tam dosya adını görüntülemek için tıklayın",
+ "multimediaviewer-download-attribution-cta-header": "Oluşturana atıf yapmanız gerekiyor",
+ "multimediaviewer-download-attribution-cta": "Nasıl yapıldığını göster",
+ "multimediaviewer-options-tooltip": "Ortam Görüntüleyici'yi etkinleştir veya devre dışı bırak",
+ "multimediaviewer-options-dialog-header": "Ortam Görüntüleyici devre dışı bırakılsın mı?",
+ "multimediaviewer-options-text-header": "Bu görüntüleme özelliğini tüm dosyalar için devre dışı bırak.",
+ "multimediaviewer-options-text-body": "Daha sonra dosya ayrıntıları sayfasından etkinleştirebilirsiniz."
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/tt-cyrl.json b/www/wiki/extensions/MultimediaViewer/i18n/tt-cyrl.json
new file mode 100644
index 00000000..24987aa2
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/tt-cyrl.json
@@ -0,0 +1,77 @@
+{
+ "@metadata": {
+ "authors": [
+ "Ильнар"
+ ]
+ },
+ "multimediaviewer-pref": "Медиа гизгеч",
+ "multimediaviewer-optin-pref": "<span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About Медиа гизгечне]</span> куллану",
+ "multimediaviewer-repository-local": "Тулырак",
+ "multimediaviewer-datetime-created": "Ясалу: $1",
+ "multimediaviewer-datetime-uploaded": "Йөкләнү: $1",
+ "multimediaviewer-credit": "$1 - $2",
+ "multimediaviewer-credit-fallback": "Автор турында мәгълүмат",
+ "multimediaviewer-multiple-authors": "тагын {{PLURAL:$1|бер автор|$1 автор}}",
+ "multimediaviewer-multiple-authors-combine": "$1 һәм $2",
+ "multimediaviewer-thumbnail-error": "Гафу итегез, файлны ачу мөмкин түгел",
+ "multimediaviewer-thumbnail-error-retry": "кабатлау",
+ "multimediaviewer-thumbnail-error-report": "хата турында хәбәр итү",
+ "multimediaviewer-license-cc-pd": "Җәмгыять мирасы",
+ "multimediaviewer-license-pd": "Җәмгыять мирасы",
+ "multimediaviewer-license-default": "Лицензияне карау",
+ "multimediaviewer-permission-title": "Шартлар турында мәгълүмат",
+ "multimediaviewer-permission-link": "хокукларны карау",
+ "multimediaviewer-permission-link-hide": "хокукларны яшерү",
+ "multimediaviewer-permission-viewmore": "Тулырак мәгълүмат",
+ "multimediaviewer-about-mmv": "Тасвирлама",
+ "multimediaviewer-discuss-mmv": "Бәхәс",
+ "multimediaviewer-help-mmv": "Ярдәм",
+ "multimediaviewer-optout-mmv": "Медиа гизгечне ябу",
+ "multimediaviewer-optin-mmv": "Медиа гизгечне ачу",
+ "multimediaviewer-optout-pending-mmv": "Медиа гизгечне ябу",
+ "multimediaviewer-optin-pending-mmv": "Медиа гизгечне ачу",
+ "multimediaviewer-optin-help": "Рәсемнәрне курсәтү өчен медиа гизгеч кулланылачак",
+ "multimediaviewer-geolocation": "Географик урыны: $1",
+ "multimediaviewer-reuse-link": "Файлны өлү яисә бүлешү",
+ "multimediaviewer-reuse-loading-placeholder": "Йөкләү...",
+ "multimediaviewer-share-tab": "Бүлешү",
+ "multimediaviewer-embed-tab": "Кую",
+ "multimediaviewer-download-link": "Файлны йөкләү",
+ "multimediaviewer-download-preview-link-title": "Гизгечтә карау",
+ "multimediaviewer-download-original-button-name": "Төп файлны алу",
+ "multimediaviewer-download-small-button-name": "Кече зурлыкта алу",
+ "multimediaviewer-download-medium-button-name": "Уртача зурлыкта алу",
+ "multimediaviewer-download-large-button-name": "Зур зурлыкта алу",
+ "multimediaviewer-embed-wt": "Викитекст",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-text-embed-credit-text-bl": "Автор: $1, $2, $3",
+ "multimediaviewer-text-embed-credit-text-b": "Автор: $1, $2",
+ "multimediaviewer-html-embed-credit-text-bl": "Автор: $1, $2, $3",
+ "multimediaviewer-html-embed-credit-text-b": "Автор: $1, $2",
+ "multimediaviewer-embed-byline": "Кулланучы $1",
+ "multimediaviewer-embed-license": "$1 лицензиясе астында.",
+ "multimediaviewer-embed-via": "$1 аркылы.",
+ "multimediaviewer-original-embed-dimensions": "Төп файл $1",
+ "multimediaviewer-large-embed-dimensions": "Зур $1",
+ "multimediaviewer-medium-embed-dimensions": "Уратача $1",
+ "multimediaviewer-small-embed-dimensions": "Кече $1",
+ "multimediaviewer-view-expanded": "Медиа гизгечтә ачу",
+ "multimediaviewer-view-config": "Көйләү",
+ "multimediaviewer-close-popup-text": "Коралны ябу (Esc)",
+ "multimediaviewer-fullscreen-popup-text": "Тулы экранда күрсәтү",
+ "multimediaviewer-defullscreen-popup-text": "Тулы экран режимыннан чыгу",
+ "multimediaviewer-title-popup-text": "Тасвирлама",
+ "multimediaviewer-credit-popup-text": "Авторы һәм чыганагы турында мәгълүмат",
+ "multimediaviewer-title-popup-text-more": "Тулы тасвирны карау",
+ "multimediaviewer-credit-popup-text-more": "Автор һәм чыганак турында тулырак мәгълүмат",
+ "multimediaviewer-download-attribution-cta-header": "Сезга авторын күрсәтү кирәк",
+ "multimediaviewer-attr-plain": "Гади",
+ "multimediaviewer-options-dialog-header": "Медиа гизгечне ябаргамы?",
+ "multimediaviewer-options-text-header": "Әлеге мөмкинлекне бөтен файллар өчен дә сүндерергә.",
+ "multimediaviewer-options-text-body": "Кирәк чакта, сез аны файл турындагы мәгълүмат битендә куша аласыз.",
+ "multimediaviewer-options-learn-more": "Күбрәк белү",
+ "multimediaviewer-option-submit-button": "Медиа гизгечне ябу",
+ "multimediaviewer-option-cancel-button": "Баш тарту",
+ "multimediaviewer-enable-dialog-header": "Медиа гизгечне ачу?",
+ "multimediaviewer-enable-submit-button": "Медиа гизгечне ачу"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/uk.json b/www/wiki/extensions/MultimediaViewer/i18n/uk.json
new file mode 100644
index 00000000..78d35595
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/uk.json
@@ -0,0 +1,133 @@
+{
+ "@metadata": {
+ "authors": [
+ "Andriykopanytsia",
+ "Ата",
+ "Sergento",
+ "Olion",
+ "Piramidion",
+ "Macofe",
+ "Base"
+ ]
+ },
+ "multimediaviewer-desc": "Розгорнути мініатюри в більшому розмірі у лайтбоксі.",
+ "multimediaviewer-pref": "Медіа переглядач",
+ "multimediaviewer-pref-desc": "Поліпшити ваші враження від перегляду мультимедіа з цим новим інструментом. Він відображає зображення у більшому розмірі на сторінках, які мають ескізи. Зображення показані у кращому накладенні і також відображаються в натуральну величину.",
+ "multimediaviewer-optin-pref": "Увімкнути <span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About Медіапереглядач]</span>",
+ "multimediaviewer-file-page": "Перейти на сторінку відповідного файлу",
+ "multimediaviewer-repository-local": "Докладніше",
+ "multimediaviewer-datetime-created": "Створено $1",
+ "multimediaviewer-datetime-uploaded": "Завантажено $1",
+ "multimediaviewer-credit-fallback": "Переглянути інформацію про автора",
+ "multimediaviewer-multiple-authors": "{{PLURAL:$1|ще один автор|$1 інші автори|$1 інших авторів}}",
+ "multimediaviewer-multiple-authors-combine": "$1 і $2",
+ "multimediaviewer-metadata-error": "Не вдалося завантажити дані зображення (помилка: $1)",
+ "multimediaviewer-thumbnail-error": "Вибачте, файл не можна відобразити",
+ "multimediaviewer-thumbnail-error-description": "Схоже, виникла технічна проблема. Ви можете $1 або $3, якщо вона збережеться. Помилка: $2",
+ "multimediaviewer-thumbnail-error-retry": "повторити",
+ "multimediaviewer-thumbnail-error-report": "повідомити про проблему",
+ "multimediaviewer-license-cc-pd": "Суспільне надбання",
+ "multimediaviewer-license-pd": "Суспільне надбання (Public Domain)",
+ "multimediaviewer-license-default": "Перегляд ліцензії",
+ "multimediaviewer-permission-title": "Відомості про дозвіл",
+ "multimediaviewer-permission-link": "перегляд умов",
+ "multimediaviewer-permission-link-hide": "приховати умови",
+ "multimediaviewer-permission-viewmore": "Показати докладно",
+ "multimediaviewer-restriction-2257": "Це зображення містить відвертий сексуальний вміст, який може підпадати під дію Закону США про захист дітей і примус до непристойності.",
+ "multimediaviewer-restriction-aus-reserve": "Це зображення є фотографією із заповідника Австралійського Союзу і не може використовуватися у комерційних цілях без дозволу.",
+ "multimediaviewer-restriction-communist": "Це зображення містить комуністичні символи, які можуть бути заборонені в деяких країнах.",
+ "multimediaviewer-restriction-costume": "Цей зображення містить костюми і може бути предметом юридичних обмежень.",
+ "multimediaviewer-restriction-currency": "Це зображення є зображенням грошової одиниці і може бути предметом юридичних обмежень.",
+ "multimediaviewer-restriction-design": "Дизайн предмета цього зображення може бути захищений авторським правом і бути предметом юридичних обмежень.",
+ "multimediaviewer-restriction-fan-art": "Це зображення є фан-артом, його повторне використання може бути предметом юридичних обмежень.",
+ "multimediaviewer-restriction-ihl": "Це зображення містить символи, заборонені міжнародним гуманітарним правом.",
+ "multimediaviewer-restriction-insignia": "Це зображення містить офіційні знаки, які можуть бути предметом юридичних обмежень.",
+ "multimediaviewer-restriction-ita-mibac": "Це зображення відтворює майно, що належить до культурної спадщини Італії, й підпадає під обмеження за італійськими законами.",
+ "multimediaviewer-restriction-nazi": "Це зображення містить нацистську або іншу фашистську символіку, яка може бути заборонена в деяких країнах.",
+ "multimediaviewer-restriction-personality": "Це зображення містить осіб, які можуть мати права, що юридично обмежують певне повторне використання зображення без згоди.",
+ "multimediaviewer-restriction-trademarked": "Це зображення має вміст, що може бути об'єктом законів про товарні знаки.",
+ "multimediaviewer-restriction-default": "Це зображення може бути обмежене правовими нормами за межами авторського права. Див. сторінку опису файлу для детальнішої інформації.",
+ "multimediaviewer-restriction-default-and-others": "Це зображення може бути ще більш обмежене іншими правовими нормами за межами авторського права. Див. сторінку опису файлу для інформації.",
+ "multimediaviewer-about-mmv": "Про програму",
+ "multimediaviewer-discuss-mmv": "Обговорення",
+ "multimediaviewer-help-mmv": "Допомога",
+ "multimediaviewer-optout-mmv": "Вимкнути медіапереглядач",
+ "multimediaviewer-optin-mmv": "Увімкнути медіапереглядач",
+ "multimediaviewer-optout-pending-mmv": "Вимкнення медіапереглядача",
+ "multimediaviewer-optin-pending-mmv": "Увімкнення медіапереглядача",
+ "multimediaviewer-optout-help": "Медіаперелядач більше не використовуватиметься для показу зображень. Щоб використовувати його знову, натисніть кнопку «{{int:multimediaviewer-view-expanded}}» поруч з будь-яким зображенням. Далі натисніть «{{int:multimediaviewer-optin-mmv}}».",
+ "multimediaviewer-optin-help": "Медіапереглядач використовуватиметься для показу зображень.",
+ "multimediaviewer-geolocation": "Розташування:$1",
+ "multimediaviewer-reuse-link": "Поділитися чи вставити цей файл",
+ "multimediaviewer-reuse-loading-placeholder": "Завантаження...",
+ "multimediaviewer-reuse-copy-share": "Виділіть і скопіюйте (якщо підтримується) посилання для поширення цього файлу",
+ "multimediaviewer-reuse-copy-embed": "Виділіть і скопіюйте (якщо підтримується) цей код для вбудовування цього файлу",
+ "multimediaviewer-share-tab": "Поділитись",
+ "multimediaviewer-embed-tab": "Вбудований",
+ "multimediaviewer-download-link": "Завантажити цей файл",
+ "multimediaviewer-download-preview-link-title": "Переглянути в браузері",
+ "multimediaviewer-download-original-button-name": "Завантажити початковий файл",
+ "multimediaviewer-download-small-button-name": "Завантажити в малому розмірі",
+ "multimediaviewer-download-medium-button-name": "Завантажити в середньому розмірі",
+ "multimediaviewer-download-large-button-name": "Завантажити у великому розмірі",
+ "multimediaviewer-link-to-page": "Посилання на сторінку опису файлу",
+ "multimediaviewer-link-to-file": "Посилання на оригінал файлу",
+ "multimediaviewer-share-explanation": "Скопіюйте і вільно діліться посиланням",
+ "multimediaviewer-embed-wt": "Вікітекст",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-embed-explanation": "Використовуйте цей код, щоб додати файл на сторінку",
+ "multimediaviewer-text-embed-credit-text-bl": "Автор: $1, $2, $3",
+ "multimediaviewer-text-embed-credit-text-b": "Автор: $1, $2",
+ "multimediaviewer-html-embed-credit-text-bl": "Автор: $1, $2, $3",
+ "multimediaviewer-html-embed-credit-text-b": "Автор: $1, $2",
+ "multimediaviewer-html-embed-credit-link-text": "Посилання",
+ "multimediaviewer-embed-byline": "Від $1",
+ "multimediaviewer-embed-license": "Під ліцензією $1.",
+ "multimediaviewer-embed-via": "Через $1.",
+ "multimediaviewer-default-embed-dimensions": "Типовий розмір мініатюри",
+ "multimediaviewer-original-embed-dimensions": "Початковий файл $1",
+ "multimediaviewer-large-embed-dimensions": "Великий $1",
+ "multimediaviewer-medium-embed-dimensions": "Середній $1",
+ "multimediaviewer-small-embed-dimensions": "Малий $1",
+ "multimediaviewer-description-page-button-text": "Докладніше про цей файл",
+ "multimediaviewer-description-page-popup-text": "Детальніше про цей файл на $1",
+ "multimediaviewer-commons-subtitle": "Сховище вільних файлів",
+ "multimediaviewer-view-expanded": "Відкрити в Медіапереглядачі.",
+ "multimediaviewer-view-config": "Налаштування",
+ "multimediaviewer-close-popup-text": "Закрити цей інструмент (Esc)",
+ "multimediaviewer-fullscreen-popup-text": "Показати на весь екран",
+ "multimediaviewer-defullscreen-popup-text": "Вийти з повноекранного режиму",
+ "multimediaviewer-next-image-alt-text": "Показати наступне зображення",
+ "multimediaviewer-prev-image-alt-text": "Показати попереднє зображення",
+ "multimediaviewer-title-popup-text": "Опис файлу",
+ "multimediaviewer-credit-popup-text": "Інформація про автора і джерело",
+ "multimediaviewer-title-popup-text-more": "Переглянути опис файлу повністю",
+ "multimediaviewer-credit-popup-text-more": "Переглянути інформацію про автора та джерело повністю",
+ "multimediaviewer-download-attribution-cta-header": "Вам потрібно вказати автора",
+ "multimediaviewer-download-optional-attribution-cta-header": "Ви можете вказати ім'я автора",
+ "multimediaviewer-download-attribution-cta": "Показати мені, як",
+ "multimediaviewer-download-attribution-copy": "Виділіть та скопіюйте (якщо підтримується) текст атрибуції цього файлу",
+ "multimediaviewer-reuse-warning-deletion": "Цей файл номіновано на вилучення.",
+ "multimediaviewer-reuse-warning-nonfree": "Цей файл не має вільної ліцензії.",
+ "multimediaviewer-reuse-warning-noattribution": "Цей файл не має інформації про авторство.",
+ "multimediaviewer-reuse-warning-generic": "Перевірте [$1 його подробиці], перед тим як використовувати.",
+ "multimediaviewer-attr-plain": "Простий текст",
+ "multimediaviewer-options-tooltip": "Увімкнути або вимкнути Медіапереглядач",
+ "multimediaviewer-options-dialog-header": "Вимкнути Медіапереглядач?",
+ "multimediaviewer-options-text-header": "Вимкнути цю функцію перегляду для всіх файлів.",
+ "multimediaviewer-options-text-body": "Пізніше Ви зможете увімкнути її на сторінці опису файлу.",
+ "multimediaviewer-options-learn-more": "Дізнатися більше",
+ "multimediaviewer-option-submit-button": "Вимкнути Медіапереглядач",
+ "multimediaviewer-option-cancel-button": "Скасувати",
+ "multimediaviewer-disable-confirmation-header": "Ви вимкнули Медіапереглядач",
+ "multimediaviewer-disable-confirmation-text": "Наступного разу при клацанні на мініатюру файлу на сайті $1, Ви потрапите безпосередньо на сторінку опису файлу.",
+ "multimediaviewer-enable-dialog-header": "Увімкнути Медіапереглядач?",
+ "multimediaviewer-enable-text-header": "Увімкнути цю функцію перегляду для всіх медіа-файлів за замовчуванням.",
+ "multimediaviewer-enable-submit-button": "Увімкнути Медіапереглядач",
+ "multimediaviewer-enable-confirmation-header": "Ви увімкнули Медіапереглядач для всіх файлів",
+ "multimediaviewer-enable-confirmation-text": "Наступного разу при клацанні на мініатюру файлу на сайті $1, буде використано Медіапереглядач.",
+ "multimediaviewer-enable-alert": "Медіапереглядач тепер вимкнено",
+ "multimediaviewer-disable-info-title": "Ви вимкнули Медіапереглядач",
+ "multimediaviewer-disable-info": "Ви все ще можете переглядати окремі файли за допомогою Медіапереглядача.",
+ "multimediaviewer-errorreport-privacywarning": "Відомості про помилку прикріплені до звіту, який буде доступний для публічного перегляду. Якщо для Вас це незручно, Ви можете відредагувати звіт нижче та усунути всі дані, якими Ви не бажаєте ділитися."
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/ur.json b/www/wiki/extensions/MultimediaViewer/i18n/ur.json
new file mode 100644
index 00000000..41965fbe
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/ur.json
@@ -0,0 +1,70 @@
+{
+ "@metadata": {
+ "authors": [
+ "عثمان خان شاہ",
+ "Muhammad Shuaib",
+ "BukhariSaeed"
+ ]
+ },
+ "multimediaviewer-pref": "میڈیا نمائش گر",
+ "multimediaviewer-optin-pref": "<span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About میڈیا نمائش گر]</span> کو فعال کریں",
+ "multimediaviewer-file-page": "فائل کے متعلقہ صفحہ پر جائیں",
+ "multimediaviewer-repository-local": "مزید تفصیلات",
+ "multimediaviewer-datetime-created": "تخلیق شدہ: $1",
+ "multimediaviewer-datetime-uploaded": "اپلوڈ شدہ: $1",
+ "multimediaviewer-credit-fallback": "مصنف کی معلومات دیکھیں",
+ "multimediaviewer-multiple-authors-combine": "$1 اور $2",
+ "multimediaviewer-metadata-error": "تصویر کی تفصیلات لوڈ نہیں ہو سکیں (نقص: $1)",
+ "multimediaviewer-thumbnail-error": "معذرت، یہ فائل نہیں دکھائی جا سکتی۔",
+ "multimediaviewer-thumbnail-error-retry": "دوبارہ کوشش کریں",
+ "multimediaviewer-license-cc-pd": "دائرہ عام",
+ "multimediaviewer-license-pd": "دائرہ عام",
+ "multimediaviewer-license-default": "اجازت نامہ دیکھیں",
+ "multimediaviewer-permission-title": "اجازت نامے کی تفصیلات",
+ "multimediaviewer-permission-link": "شرائط دیکھیں",
+ "multimediaviewer-permission-link-hide": "شرائط چھپائیں",
+ "multimediaviewer-permission-viewmore": "مزید دیکھیں",
+ "multimediaviewer-about-mmv": "تعارف",
+ "multimediaviewer-discuss-mmv": "تبادلۂ خیال",
+ "multimediaviewer-help-mmv": "معاونت",
+ "multimediaviewer-optout-mmv": "میڈیا نمائش گر کو غیر فعال کريں",
+ "multimediaviewer-optin-mmv": "میڈیا نمائش گر کو فعال کريں",
+ "multimediaviewer-optout-pending-mmv": "میڈیا نمائش گر غیر فعال ہو رہا ہے",
+ "multimediaviewer-optin-pending-mmv": "میڈیا نمائش گر فعال ہو رہا ہے",
+ "multimediaviewer-geolocation": "مقام: $1",
+ "multimediaviewer-reuse-loading-placeholder": "لوڈ ہو رہا ہے۔۔۔",
+ "multimediaviewer-download-link": "اس فائل کو ڈاؤنلوڈ کریں",
+ "multimediaviewer-download-preview-link-title": "براؤزر میں کھولیں",
+ "multimediaviewer-download-original-button-name": "اصل فائل حاصل کریں",
+ "multimediaviewer-download-small-button-name": "چھوٹے حجم میں ڈاؤنلوڈ کریں",
+ "multimediaviewer-download-medium-button-name": "متوسط حجم میں ڈاؤنلوڈ کریں",
+ "multimediaviewer-download-large-button-name": "بڑے حجم میں ڈاؤنلوڈ کریں",
+ "multimediaviewer-link-to-page": "فائل کے صفحہ وضاحت کا ربط",
+ "multimediaviewer-link-to-file": "اصل فائل کا ربط",
+ "multimediaviewer-share-explanation": "ربط کو نقل اور نشر کریں",
+ "multimediaviewer-embed-wt": "ویکی متن",
+ "multimediaviewer-embed-html": "ایچ ٹی ایم ایل",
+ "multimediaviewer-text-embed-credit-text-bl": "از $1، $2، $3",
+ "multimediaviewer-text-embed-credit-text-b": "از $1، $2",
+ "multimediaviewer-html-embed-credit-text-bl": "از $1، $2، $3",
+ "multimediaviewer-html-embed-credit-text-b": "از $1، $2",
+ "multimediaviewer-embed-byline": "از $1",
+ "multimediaviewer-embed-license": "اجازت نامہ: $1",
+ "multimediaviewer-embed-via": "ماخذ: $1",
+ "multimediaviewer-description-page-button-text": "اس فائل کے متعلق مزید تفصیلات",
+ "multimediaviewer-view-expanded": "میڈیا نمائش گر میں کھولیں",
+ "multimediaviewer-title-popup-text": "وضاحت",
+ "multimediaviewer-credit-popup-text": "مصنف و ماخذ کی معلومات",
+ "multimediaviewer-title-popup-text-more": "مکمل وضاحت دیکھیں",
+ "multimediaviewer-options-tooltip": "میڈیا نمائش گر کو فعال یا غیر فعال کریں",
+ "multimediaviewer-options-dialog-header": "میڈیا نمائش گر کو غیر فعال کريں؟",
+ "multimediaviewer-options-learn-more": "مزید معلومات حاصل کریں",
+ "multimediaviewer-option-submit-button": "میڈیا نمائش گر کو غیر فعال کريں",
+ "multimediaviewer-option-cancel-button": "منسوخ",
+ "multimediaviewer-disable-confirmation-header": "آپ نے میڈیا نمائش گر کو غیر فعال کر دیا ہے",
+ "multimediaviewer-enable-dialog-header": "میڈیا نمائش گر کو فعال کريں؟",
+ "multimediaviewer-enable-submit-button": "میڈیا نمائش گر کو فعال کريں",
+ "multimediaviewer-enable-confirmation-header": "آپ نے تمام فائلوں کے لیے میڈیا نمائش گر کو فعال کر دیا ہے",
+ "multimediaviewer-enable-alert": "میڈیا نمائش گر اب غیر فعال ہوگیا ہے",
+ "multimediaviewer-disable-info-title": "آپ نے میڈیا نمائش گر کو غیر فعال کر دیا ہے"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/vi.json b/www/wiki/extensions/MultimediaViewer/i18n/vi.json
new file mode 100644
index 00000000..20ef6e42
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/vi.json
@@ -0,0 +1,138 @@
+{
+ "@metadata": {
+ "authors": [
+ "Minh Nguyen",
+ "Withoutaname",
+ "Max20091",
+ "Macofe",
+ "Envlh"
+ ]
+ },
+ "multimediaviewer-desc": "Mở các hình nhỏ lớn hơn trong giao diện toàn màn hình.",
+ "multimediaviewer-pref": "Cửa sổ phương tiện",
+ "multimediaviewer-pref-desc": "Cải thiện trải nghiệm xem phương tiện của bạn với công cụ mới này. Nó mở rộng các hình nhỏ để phủ lên toàn cửa sổ. Các hình ảnh được hiển thị trong giao diện toàn màn hình đẹp đẽ và cũng có thể xem kích thước gốc.",
+ "multimediaviewer-optin-pref": "Bật <span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About cửa sổ phương tiện]</span>",
+ "multimediaviewer-file-page": "Mở trang ứng với tập tin",
+ "multimediaviewer-repository-local": "Thêm chi tiết",
+ "multimediaviewer-datetime-created": "Ngày tạo: $1",
+ "multimediaviewer-datetime-uploaded": "Ngày tải lên: $1",
+ "multimediaviewer-credit": "$1 – $2",
+ "multimediaviewer-credit-fallback": "Xem thông tin tác giả",
+ "multimediaviewer-multiple-authors": "{{PLURAL:$1|một tác giả|$1 tác giả}} nữa",
+ "multimediaviewer-multiple-authors-combine": "$1 và $2",
+ "multimediaviewer-metadata-error": "Không thể tải chi tiết hình ảnh (lỗi: $1)",
+ "multimediaviewer-thumbnail-error": "Rất tiếc, không thể hiển thị tập tin",
+ "multimediaviewer-thumbnail-error-description": "Hình như đã gặp vấn đề về kỹ thuật. Bạn có thể $1 hoặc $3 nếu tiếp tục xảy ra. Lỗi: $2",
+ "multimediaviewer-thumbnail-error-retry": "thử lại",
+ "multimediaviewer-thumbnail-error-report": "báo cáo về vấn đề",
+ "multimediaviewer-license-cc-by-4.0": "CC-BY 4.0",
+ "multimediaviewer-license-cc-by-sa-4.0": "CC BY-SA 4.0",
+ "multimediaviewer-license-cc-pd": "Phạm vi công cộng",
+ "multimediaviewer-license-cc-zero": "CC0",
+ "multimediaviewer-license-pd": "Phạm vi công cộng",
+ "multimediaviewer-license-default": "Xem giấy phép",
+ "multimediaviewer-permission-title": "Chi tiết cho phép",
+ "multimediaviewer-permission-link": "xem điều khoản",
+ "multimediaviewer-permission-link-hide": "ẩn điều khiển",
+ "multimediaviewer-permission-viewmore": "Xem thêm",
+ "multimediaviewer-restriction-2257": "Hình ảnh này có nội dung khiêu dâm có thể được Đạo luật Bảo vệ Trẻ em và Ép buộc Điều lệ Tục tĩu (Child Protection and Obscenity Enforcement Act) kiểm soát tại Hoa Kỳ.",
+ "multimediaviewer-restriction-aus-reserve": "Hình ảnh này được chụp tại một khu bảo tồn của Thịnh vượng chung Úc, nên phải có giấy cấp phép để sử dụng nó với mục đích thương mại.",
+ "multimediaviewer-restriction-communist": "Hình ảnh này có dấu cộng sản có thể bị cấm tại một số quốc gia.",
+ "multimediaviewer-restriction-costume": "Hình ảnh này có người cải trang nên có thể được kiểm soát bằng pháp luật.",
+ "multimediaviewer-restriction-currency": "Hình ảnh này có tiền tệ nên có thể được kiểm soát bằng pháp luật.",
+ "multimediaviewer-restriction-design": "Thiết kế của chủ đề hình ảnh này có thể dưới bản quyền và được kiểm soát bằng pháp luật.",
+ "multimediaviewer-restriction-fan-art": "Hình ảnh này là do người hâm mộ tạo ra, nên sự tái sử dụng có thể được kiểm soát bằng pháp luật.",
+ "multimediaviewer-restriction-ihl": "Hình ảnh này có ký hiệu được kiểm soát bởi Luật Nhân đạo Quốc tế.",
+ "multimediaviewer-restriction-insignia": "Hình ảnh này có huy hiệu chính thức có thể được kiểm soát bằng pháp luật.",
+ "multimediaviewer-restriction-ita-mibac": "Hình ảnh này sao lại di sản văn hóa Ý và được kiểm soát bởi luật Ý.",
+ "multimediaviewer-restriction-nazi": "Hình ảnh này có dấu Quốc xã hoặc phát xít có thể bị cấm tại một số quốc gia.",
+ "multimediaviewer-restriction-personality": "Những người trong hình này có thể có quyền hạn chế việc tái sử dụng hình này mà không được cấp phép.",
+ "multimediaviewer-restriction-trademarked": "Công trình này có chứa tư liệu có thể là đối tượng của luật thương hiệu theo một hoặc một số bộ luật.",
+ "multimediaviewer-restriction-default": "Hình ảnh này có thể được kiểm soát bởi những điều khoản luật pháp ngoài luật quyền tác giả. Xem chi tiết tại trang miêu tả tập tin.",
+ "multimediaviewer-restriction-default-and-others": "Hình ảnh này có thể được kiểm soát thêm bởi những điều khoản luật pháp ngoài luật quyền tác giả. Xem chi tiết tại trang miêu tả tập tin.",
+ "multimediaviewer-about-mmv": "Giới thiệu",
+ "multimediaviewer-discuss-mmv": "Thảo luận",
+ "multimediaviewer-help-mmv": "Trợ giúp",
+ "multimediaviewer-optout-mmv": "Tắt cửa sổ phương tiện",
+ "multimediaviewer-optin-mmv": "Bật cửa sổ phương tiện",
+ "multimediaviewer-optout-pending-mmv": "Tắt cửa sổ phương tiện",
+ "multimediaviewer-optin-pending-mmv": "Bật cửa sổ phương tiện",
+ "multimediaviewer-optout-help": "Các hình ảnh sẽ không còn mở rộng trong cửa sổ phương tiện. Để lại sử dụng cửa sổ này, hãy bấm nút “{{int:multimediaviewer-view-expanded}}” bên cạnh hình ảnh nào đó, rồi bấm “{{int:multimediaviewer-optin-mmv}}”.",
+ "multimediaviewer-optin-help": "Các hình ảnh sẽ mở rộng trong cửa sổ phương tiện.",
+ "multimediaviewer-geoloc-north": "B",
+ "multimediaviewer-geoloc-east": "Đ",
+ "multimediaviewer-geoloc-south": "N",
+ "multimediaviewer-geoloc-west": "T",
+ "multimediaviewer-geoloc-coord": "$1°$2′$3″$4",
+ "multimediaviewer-geolocation": "Vị trí: $1",
+ "multimediaviewer-reuse-link": "Chia sẻ hoặc nhúng tập tin này",
+ "multimediaviewer-reuse-loading-placeholder": "Đang tải…",
+ "multimediaviewer-reuse-copy-share": "Chọn và sao chép (nếu được hỗ trợ) liên kết để chia sẻ tập tin này",
+ "multimediaviewer-reuse-copy-embed": "Chọn và sao chép (nếu được hỗ trợ) đoạn mã để nhúng tập tin này",
+ "multimediaviewer-share-tab": "Chia sẻ",
+ "multimediaviewer-embed-tab": "Nhúng",
+ "multimediaviewer-download-link": "Tải về tập tin này",
+ "multimediaviewer-download-preview-link-title": "Xem trong trình duyệt",
+ "multimediaviewer-download-original-button-name": "Tải về tập tin gốc",
+ "multimediaviewer-download-small-button-name": "Tải về hình nhỏ",
+ "multimediaviewer-download-medium-button-name": "Tải về hình vừa",
+ "multimediaviewer-download-large-button-name": "Tải về hình lớn",
+ "multimediaviewer-link-to-page": "Liên kết đến trang miêu tả",
+ "multimediaviewer-link-to-file": "Liên kết đến tập tin gốc",
+ "multimediaviewer-share-explanation": "Sao chép và thoải mái chia sẻ liên kết này",
+ "multimediaviewer-embed-wt": "Mã wiki",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-embed-explanation": "Nhúng tập tin bằng đoạn mã này",
+ "multimediaviewer-text-embed-credit-text-bl": "Bởi $1, $2, $3",
+ "multimediaviewer-text-embed-credit-text-b": "Bởi $1, $2",
+ "multimediaviewer-html-embed-credit-text-bl": "Bởi $1, $2, $3",
+ "multimediaviewer-html-embed-credit-text-b": "Bởi $1, $2",
+ "multimediaviewer-html-embed-credit-link-text": "Liên kết",
+ "multimediaviewer-embed-byline": "Bởi $1",
+ "multimediaviewer-embed-license": "Phát hành theo giấy phép $1.",
+ "multimediaviewer-embed-via": "Do $1 cung cấp.",
+ "multimediaviewer-default-embed-dimensions": "Kích thước hình nhỏ mặc định",
+ "multimediaviewer-original-embed-dimensions": "Tập tin gốc $1",
+ "multimediaviewer-large-embed-dimensions": "Lớn $1",
+ "multimediaviewer-medium-embed-dimensions": "Vừa $1",
+ "multimediaviewer-small-embed-dimensions": "Nhỏ $1",
+ "multimediaviewer-embed-dimensions": "$1×$2 điểm ảnh",
+ "multimediaviewer-embed-dimensions-separated": "– $1",
+ "multimediaviewer-description-page-button-text": "Thêm chi tiết về tập tin này",
+ "multimediaviewer-description-page-popup-text": "Thêm chi tiết về tập tin này tại $1",
+ "multimediaviewer-commons-subtitle": "Kho tư liệu mở",
+ "multimediaviewer-view-expanded": "Mở trong cửa sổ phương tiện",
+ "multimediaviewer-view-config": "Cấu hình",
+ "multimediaviewer-close-popup-text": "Đóng cửa sổ này (Esc)",
+ "multimediaviewer-fullscreen-popup-text": "Xem toàn màn hình",
+ "multimediaviewer-defullscreen-popup-text": "Thoát ra toàn màn hình",
+ "multimediaviewer-next-image-alt-text": "Xem hình sau",
+ "multimediaviewer-prev-image-alt-text": "Xem hình trước",
+ "multimediaviewer-title-popup-text": "Miêu tả",
+ "multimediaviewer-credit-popup-text": "Thông tin tác giả và nguồn gốc",
+ "multimediaviewer-title-popup-text-more": "Xem miêu tả đầy đủ",
+ "multimediaviewer-credit-popup-text-more": "Xem tác giả và nguồn gốc đầy đủ",
+ "multimediaviewer-download-attribution-cta-header": "Bạn cần phải ghi công tác giả",
+ "multimediaviewer-download-optional-attribution-cta-header": "Bạn có thể ghi công tác giả",
+ "multimediaviewer-download-attribution-cta": "Chỉ tôi làm thế nào",
+ "multimediaviewer-download-attribution-copy": "Chọn và sao chép (nếu được hỗ trợ) văn bản ghi công của tập tin này",
+ "multimediaviewer-attr-plain": "Văn bản thuần",
+ "multimediaviewer-options-tooltip": "Bật/tắt cửa sổ phương tiện",
+ "multimediaviewer-options-dialog-header": "Tắt cửa sổ phương tiện?",
+ "multimediaviewer-options-text-header": "Bỏ qua cửa sổ phương tiện cho tất cả các tập tin.",
+ "multimediaviewer-options-text-body": "Bạn có thể bật nó lên về sau trong trang miêu tả tập tin.",
+ "multimediaviewer-options-learn-more": "Tìm hiểu thêm",
+ "multimediaviewer-option-submit-button": "Tắt cửa sổ phương tiện",
+ "multimediaviewer-option-cancel-button": "Hủy bỏ",
+ "multimediaviewer-disable-confirmation-header": "Bạn đã tắt cửa sổ phương tiện",
+ "multimediaviewer-disable-confirmation-text": "Lần sau khi bạn nhấn chuột vào một hình nhỏ trên $1, bạn sẽ được dẫn trực tiếp đến trang có tất cả các chi tiết của tập tin.",
+ "multimediaviewer-enable-dialog-header": "Bật cửa sổ phương tiện?",
+ "multimediaviewer-enable-text-header": "Xem tất cả các tập tin trong cửa sổ phương tiện theo mặc định.",
+ "multimediaviewer-enable-submit-button": "Bật cửa sổ phương tiện",
+ "multimediaviewer-enable-confirmation-header": "Bạn đã bật lên cửa sổ phương tiện cho tất cả các tập tin",
+ "multimediaviewer-enable-confirmation-text": "Lần sau khi bạn nhấn chuột vào một hình nhỏ trên $1, nó sẽ xuất hiện trong cửa sổ phương tiện.",
+ "multimediaviewer-enable-alert": "Cửa sổ phương tiện đã tắt",
+ "multimediaviewer-disable-info-title": "Đã tắt cửa sổ phương tiện",
+ "multimediaviewer-disable-info": "Bạn vẫn có thể xem tập tin riêng trong cửa sổ phương tiện.",
+ "multimediaviewer-errorreport-privacywarning": "Các chi tiết lỗi được đính kèm vào bản báo cáo công khai. Nếu bạn không đồng ý với điều này, bạn có thể sửa đổi bản báo cáo bên dưới và xóa tất cả dữ liệu mà bạn không muốn chia sẻ."
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/vo.json b/www/wiki/extensions/MultimediaViewer/i18n/vo.json
new file mode 100644
index 00000000..2b6f9f91
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/vo.json
@@ -0,0 +1,9 @@
+{
+ "@metadata": {
+ "authors": [
+ "Malafaya"
+ ]
+ },
+ "multimediaviewer-datetime-created": "Pejafon tü $1",
+ "multimediaviewer-datetime-uploaded": "Pelöpükon tü $1"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/wa.json b/www/wiki/extensions/MultimediaViewer/i18n/wa.json
new file mode 100644
index 00000000..fa15af7a
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/wa.json
@@ -0,0 +1,8 @@
+{
+ "@metadata": {
+ "authors": [
+ "Srtxg"
+ ]
+ },
+ "multimediaviewer-optin-pref": "Mete en alaedje li <span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About håyneu di medias]</span>"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/yi.json b/www/wiki/extensions/MultimediaViewer/i18n/yi.json
new file mode 100644
index 00000000..439a77ba
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/yi.json
@@ -0,0 +1,55 @@
+{
+ "@metadata": {
+ "authors": [
+ "פוילישער",
+ "Har-wradim"
+ ]
+ },
+ "multimediaviewer-pref": "מעדיע־ווייזער",
+ "multimediaviewer-repository-local": "מער פּרטים",
+ "multimediaviewer-datetime-created": "געשאפן: $1",
+ "multimediaviewer-datetime-uploaded": "ארויפגעלאדן: $1",
+ "multimediaviewer-credit-fallback": "באקוקן שאפֿער אינפארמאציע",
+ "multimediaviewer-multiple-authors": "נאך {{PLURAL:$1|איין|$1}} שאפער",
+ "multimediaviewer-multiple-authors-combine": "$1 און $2",
+ "multimediaviewer-thumbnail-error": "אנטשולדיקט, מען קען נישט ווייזן די טעקע",
+ "multimediaviewer-thumbnail-error-description": "עס זעט אויס אז ס׳איז פֿאראן א טעכנישער פראבלעם. איר קענט $1 אדער $3 טאמער עס האלט זיך אן ווייטער. פֿעלער: $2",
+ "multimediaviewer-thumbnail-error-retry": "פרובירן ווידער",
+ "multimediaviewer-thumbnail-error-report": "מעלדן דעם פראבלעם",
+ "multimediaviewer-license-default": "באקוקן ליצענץ",
+ "multimediaviewer-permission-title": "ערלויבניש פרטים",
+ "multimediaviewer-permission-link": "באקוקן טערמינען",
+ "multimediaviewer-permission-link-hide": "באהאלטן באדינגונגען",
+ "multimediaviewer-permission-viewmore": "ווײזן נאך",
+ "multimediaviewer-about-mmv": "וועגן",
+ "multimediaviewer-discuss-mmv": "שמועס",
+ "multimediaviewer-help-mmv": "הילף",
+ "multimediaviewer-optin-mmv": "אקטיווירן מעדיע־ווייזער",
+ "multimediaviewer-optin-help": "מען וועט ניצן מעדיע־באקוקער צו ווײַזן בילדער.",
+ "multimediaviewer-geolocation": "לאקאציע: $1",
+ "multimediaviewer-reuse-loading-placeholder": "לאָדנדיק…",
+ "multimediaviewer-share-tab": "טיילן",
+ "multimediaviewer-download-link": "אַראָפלאָדן די טעקע",
+ "multimediaviewer-download-preview-link-title": "באקוקן אין בלעטערער",
+ "multimediaviewer-download-original-button-name": "אַראָפלאָדן אריגינעלע טעקע",
+ "multimediaviewer-embed-wt": "וויקיטעקסט",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-text-embed-credit-text-bl": "פון $1, $2, $3",
+ "multimediaviewer-text-embed-credit-text-b": "פון $1, $2",
+ "multimediaviewer-html-embed-credit-text-bl": "פון $1, $2, $3",
+ "multimediaviewer-html-embed-credit-text-b": "פון $1, $2",
+ "multimediaviewer-html-embed-credit-link-text": "לינק",
+ "multimediaviewer-embed-byline": "פֿון $1",
+ "multimediaviewer-embed-license": "ליצענצירט אונטער $1.",
+ "multimediaviewer-embed-via": "דורך $1.",
+ "multimediaviewer-large-embed-dimensions": "גרויס $1",
+ "multimediaviewer-medium-embed-dimensions": "מיטל $1",
+ "multimediaviewer-small-embed-dimensions": "קליין $1",
+ "multimediaviewer-description-page-button-text": "נאך פרטים וועגן דער טעקטע",
+ "multimediaviewer-view-expanded": "עפֿענען אין מעדיע־ווייזער",
+ "multimediaviewer-view-config": "קאנפֿיגוראציע",
+ "multimediaviewer-title-popup-text": "באַשרײבונג",
+ "multimediaviewer-download-attribution-cta": "ווײַז מיך וויאזוי",
+ "multimediaviewer-attr-plain": "קלארטעקסט",
+ "multimediaviewer-option-cancel-button": "אַנולירן"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/zh-hans.json b/www/wiki/extensions/MultimediaViewer/i18n/zh-hans.json
new file mode 100644
index 00000000..0f7fcf46
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/zh-hans.json
@@ -0,0 +1,159 @@
+{
+ "@metadata": {
+ "authors": [
+ "Liuxinyu970226",
+ "Qiyue2001",
+ "Shizhao",
+ "Stieizc",
+ "Xiaomingyan",
+ "Yfdyh000",
+ "Hudafu",
+ "Cwek",
+ "Duolaimi",
+ "NigelSoft",
+ "Liangent",
+ "Papapasan",
+ "Linforest",
+ "Hydra",
+ "飞舞回堂前"
+ ]
+ },
+ "multimediaviewer-desc": "在全屏界面中以较大尺寸显示缩略图。",
+ "multimediaviewer-pref": "媒体查看器",
+ "multimediaviewer-pref-desc": "使用这个新工具改善您的多媒体浏览体验。它能以更大的尺寸显示页面中的缩略图。图像将显示于一个漂亮的全屏界面浮层中,并能以完整尺寸查看。",
+ "multimediaviewer-optin-pref": "启用<span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About 媒体查看器]</span>",
+ "multimediaviewer-file-page": "前往对应的文件页面",
+ "multimediaviewer-repository-local": "更多详情",
+ "multimediaviewer-datetime-created": "创建于:$1",
+ "multimediaviewer-datetime-uploaded": "上传于:$1",
+ "multimediaviewer-credit-fallback": "查看作者信息",
+ "multimediaviewer-multiple-authors": "{{PLURAL:$1|$1位更多作者}}",
+ "multimediaviewer-multiple-authors-combine": "$1和$2",
+ "multimediaviewer-metadata-error": "无法加载图片的详细信息(错误:$1)",
+ "multimediaviewer-thumbnail-error": "抱歉,文件无法显示",
+ "multimediaviewer-thumbnail-error-description": "看起来发生了技术问题。您可以$1,或如果仍然发生,可$3。错误:$2",
+ "multimediaviewer-thumbnail-error-retry": "重试",
+ "multimediaviewer-thumbnail-error-report": "报告问题",
+ "multimediaviewer-license-cc-by-1.0": "CC BY 1.0",
+ "multimediaviewer-license-cc-by-sa-1.0": "CC BY-SA 1.0",
+ "multimediaviewer-license-cc-by-2.0": "CC BY 2.0",
+ "multimediaviewer-license-cc-by-sa-2.0": "CC BY-SA 2.0",
+ "multimediaviewer-license-cc-by-2.1": "CC BY 2.1",
+ "multimediaviewer-license-cc-by-sa-2.1": "CC BY-SA 2.1",
+ "multimediaviewer-license-cc-by-2.5": "CC BY 2.5",
+ "multimediaviewer-license-cc-by-sa-2.5": "CC BY-SA 2.5",
+ "multimediaviewer-license-cc-by-3.0": "CC BY 3.0",
+ "multimediaviewer-license-cc-by-sa-3.0": "CC BY-SA 3.0",
+ "multimediaviewer-license-cc-by-4.0": "CC BY 4.0",
+ "multimediaviewer-license-cc-by-sa-4.0": "CC BY-SA 4.0",
+ "multimediaviewer-license-cc-pd": "公有领域",
+ "multimediaviewer-license-pd": "公有领域",
+ "multimediaviewer-license-default": "查看许可协议",
+ "multimediaviewer-permission-title": "许可协议详情",
+ "multimediaviewer-permission-link": "查看条款",
+ "multimediaviewer-permission-link-hide": "隐藏条款",
+ "multimediaviewer-permission-viewmore": "查看更多",
+ "multimediaviewer-restriction-2257": "此图片包含色情内容,它可能受美国Child Protection and Obscenity Enforcement Act法律的管制。",
+ "multimediaviewer-restriction-aus-reserve": "此图片是在澳大利亚联邦储备机构中拍摄的,并因此在未获许可的情况下不能用于商业性使用。",
+ "multimediaviewer-restriction-communist": "此图片包含共产主义标志,它可能被一些国家禁止使用。",
+ "multimediaviewer-restriction-costume": "此图片描述服装,并可能受法律限制。",
+ "multimediaviewer-restriction-currency": "此图片表现了对货币单位的描述,并可能受法律限制。",
+ "multimediaviewer-restriction-design": "此图片主题的设计可能受到版权保护,并受法律限制。",
+ "multimediaviewer-restriction-fan-art": "此图片是一件爱好者作品,并在再利用时可能受到法律限制。",
+ "multimediaviewer-restriction-ihl": "此图片包含受国际人道主义法限制的符号。",
+ "multimediaviewer-restriction-insignia": "此图片包含官方标志,并可能受法律限制。",
+ "multimediaviewer-restriction-ita-mibac": "此图片再现了意大利文化遗产,并受意大利法律限制。",
+ "multimediaviewer-restriction-nazi": "此图片包含纳粹或其他法西斯标志,它可能被一些国家禁止使用。",
+ "multimediaviewer-restriction-personality": "此图片包含人物,可能有合法权利限制未经许可的图片再利用。",
+ "multimediaviewer-restriction-trademarked": "此图片包含受商标法管理的内容。",
+ "multimediaviewer-restriction-default": "此图片可能受版权法以外的法律法规限制。请参见文件说明页面以获取详细信息。",
+ "multimediaviewer-restriction-default-and-others": "此图片可能受版权法以外的其他法律法规的进一步限制。请参见文件说明页面以获取详细信息。",
+ "multimediaviewer-about-mmv": "关于",
+ "multimediaviewer-discuss-mmv": "讨论",
+ "multimediaviewer-help-mmv": "帮助",
+ "multimediaviewer-optout-mmv": "停用媒体查看器",
+ "multimediaviewer-optin-mmv": "启用媒体查看器",
+ "multimediaviewer-optout-pending-mmv": "正在停用媒体查看器",
+ "multimediaviewer-optin-pending-mmv": "正在启用媒体查看器",
+ "multimediaviewer-optout-help": "媒体查看器将不再用于展示图像。要再次使用,请点击任意图像旁的“{{int:multimediaviewer-view-expanded}}”按钮,然后点击“{{int:multimediaviewer-optin-mmv}}”。",
+ "multimediaviewer-optin-help": "媒体查看器将会用于展示图像。",
+ "multimediaviewer-geoloc-north": "N",
+ "multimediaviewer-geoloc-east": "E",
+ "multimediaviewer-geoloc-south": "S",
+ "multimediaviewer-geoloc-west": "W",
+ "multimediaviewer-geolocation": "位置:$1",
+ "multimediaviewer-reuse-link": "分享或嵌入该文件",
+ "multimediaviewer-reuse-loading-placeholder": "正在载入…",
+ "multimediaviewer-reuse-copy-share": "选择并复制(如果支持)链接以分享该文件",
+ "multimediaviewer-reuse-copy-embed": "选择并复制(如果支持)代码以嵌入该文件",
+ "multimediaviewer-share-tab": "分享",
+ "multimediaviewer-embed-tab": "嵌入",
+ "multimediaviewer-download-link": "下载此文件",
+ "multimediaviewer-download-preview-link-title": "在浏览器中预览",
+ "multimediaviewer-download-original-button-name": "下载原始文件",
+ "multimediaviewer-download-small-button-name": "下载小尺寸",
+ "multimediaviewer-download-medium-button-name": "下载中等尺寸",
+ "multimediaviewer-download-large-button-name": "下载大尺寸",
+ "multimediaviewer-link-to-page": "文件说明页面的链接",
+ "multimediaviewer-link-to-file": "原始文件的链接",
+ "multimediaviewer-share-explanation": "复制并自由分享本链接",
+ "multimediaviewer-embed-wt": "Wiki文本",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-embed-explanation": "使用这些代码嵌入该文件",
+ "multimediaviewer-text-embed-credit-text-bl": "由$1,$2,$3",
+ "multimediaviewer-text-embed-credit-text-b": "由$1,$2",
+ "multimediaviewer-html-embed-credit-text-bl": "由$1,$2,$3",
+ "multimediaviewer-html-embed-credit-text-b": "由$1,$2",
+ "multimediaviewer-html-embed-credit-link-text": "链接",
+ "multimediaviewer-embed-byline": "作者$1",
+ "multimediaviewer-embed-license": "采用$1授权。",
+ "multimediaviewer-embed-via": "来自$1。",
+ "multimediaviewer-default-embed-dimensions": "默认缩略图尺寸",
+ "multimediaviewer-original-embed-dimensions": "原始文件$1",
+ "multimediaviewer-large-embed-dimensions": "大 $1",
+ "multimediaviewer-medium-embed-dimensions": "中 $1",
+ "multimediaviewer-small-embed-dimensions": "小 $1",
+ "multimediaviewer-embed-dimensions": "$1 × $2 像素",
+ "multimediaviewer-embed-dimensions-separated": "- $1",
+ "multimediaviewer-description-page-button-text": "更多关于此文件的详情",
+ "multimediaviewer-description-page-popup-text": "在$1了解该文件详情",
+ "multimediaviewer-commons-subtitle": "自由的媒体存储库",
+ "multimediaviewer-view-expanded": "在媒体查看器中打开",
+ "multimediaviewer-view-config": "配置",
+ "multimediaviewer-close-popup-text": "关闭此工具(或按“Esc”键退出)",
+ "multimediaviewer-fullscreen-popup-text": "全屏显示",
+ "multimediaviewer-defullscreen-popup-text": "退出全屏",
+ "multimediaviewer-next-image-alt-text": "显示下一张图片",
+ "multimediaviewer-prev-image-alt-text": "显示上一张图片",
+ "multimediaviewer-title-popup-text": "说明",
+ "multimediaviewer-credit-popup-text": "作者和来源信息",
+ "multimediaviewer-title-popup-text-more": "显示完整说明",
+ "multimediaviewer-credit-popup-text-more": "显示完整的作者和来源",
+ "multimediaviewer-download-attribution-cta-header": "您需要为作者署名",
+ "multimediaviewer-download-optional-attribution-cta-header": "您可以为作者署名",
+ "multimediaviewer-download-attribution-cta": "告诉我如何",
+ "multimediaviewer-download-attribution-copy": "选择并复制(如果支持)此文件的署名文本",
+ "multimediaviewer-reuse-warning-deletion": "文件被标记提删。",
+ "multimediaviewer-reuse-warning-nonfree": "文件没有自由许可协议。",
+ "multimediaviewer-reuse-warning-noattribution": "文件没有署名信息。",
+ "multimediaviewer-reuse-warning-generic": "在使用前请检查[$1 详情]。",
+ "multimediaviewer-attr-plain": "无格式",
+ "multimediaviewer-options-tooltip": "启用或禁用媒体查看器",
+ "multimediaviewer-options-dialog-header": "禁用媒体查看器?",
+ "multimediaviewer-options-text-header": "对所有文件跳过查看功能",
+ "multimediaviewer-options-text-body": "您可以稍后通过文件详细说明页面启用它。",
+ "multimediaviewer-options-learn-more": "了解更多",
+ "multimediaviewer-option-submit-button": "禁用媒体查看器",
+ "multimediaviewer-option-cancel-button": "取消",
+ "multimediaviewer-disable-confirmation-header": "您已禁用媒体查看器",
+ "multimediaviewer-disable-confirmation-text": "下一次在您点击$1上的缩略图时,您将直接查看文件的详细信息。",
+ "multimediaviewer-enable-dialog-header": "启用媒体查看器?",
+ "multimediaviewer-enable-text-header": "默认对所有文件启用此媒体查看功能。",
+ "multimediaviewer-enable-submit-button": "启用媒体查看器",
+ "multimediaviewer-enable-confirmation-header": "您已对所有文件启用媒体查看器",
+ "multimediaviewer-enable-confirmation-text": "下次当您在$1点击一个缩略图时,媒体查看器将被使用。",
+ "multimediaviewer-enable-alert": "媒体查看器已禁用",
+ "multimediaviewer-disable-info-title": "您已禁用媒体查看器",
+ "multimediaviewer-disable-info": "您仍可通过媒体查看器查看个别文件。",
+ "multimediaviewer-errorreport-privacywarning": "错误的详细信息已附加于报告中,它将公开可见。如果您对此感到不适,您可以编辑下方的报告并移除所有您不希望分享的数据。"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/i18n/zh-hant.json b/www/wiki/extensions/MultimediaViewer/i18n/zh-hant.json
new file mode 100644
index 00000000..8a0a122c
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/i18n/zh-hant.json
@@ -0,0 +1,116 @@
+{
+ "@metadata": {
+ "authors": [
+ "Liuxinyu970226",
+ "Simon Shek",
+ "Liangent",
+ "Cwlin0416",
+ "LNDDYL",
+ "Alexsh"
+ ]
+ },
+ "multimediaviewer-desc": "使用全螢幕介面以較大尺寸的方式顯示展開後的縮圖。",
+ "multimediaviewer-pref": "媒體檢視器",
+ "multimediaviewer-pref-desc": "這個新的工具可以改善您的多媒體瀏覽體驗。它會在有縮圖的網頁中以更大的尺寸顯示影像;影像將以浮動視窗呈現,您也可以選擇檢視原始大小的圖片。",
+ "multimediaviewer-optin-pref": "開啟 <span class=\"plainlinks\">[https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About 媒體檢視器]</span>",
+ "multimediaviewer-file-page": "前往相關的檔案頁面",
+ "multimediaviewer-repository-local": "更多詳細資料",
+ "multimediaviewer-datetime-created": "建立於:$1",
+ "multimediaviewer-datetime-uploaded": "上傳於:$1",
+ "multimediaviewer-credit-fallback": "檢視作者資訊",
+ "multimediaviewer-multiple-authors": "{{PLURAL:$1|還有 1 位作者|還有 $1 位作者}}",
+ "multimediaviewer-multiple-authors-combine": "$1 及 $2",
+ "multimediaviewer-metadata-error": "無法讀取圖片資料 (錯誤:$1)",
+ "multimediaviewer-thumbnail-error": "抱歉,檔案無法正常顯示",
+ "multimediaviewer-thumbnail-error-description": "似乎發生技術性問題,您可以 $1,若問題仍然存在,可 $3。錯誤:$2",
+ "multimediaviewer-thumbnail-error-retry": "重試",
+ "multimediaviewer-thumbnail-error-report": "回報問題",
+ "multimediaviewer-license-cc-by-sa-1.0": "CC BY-SA 1.0",
+ "multimediaviewer-license-cc-by-sa-2.0": "CC BY-SA 2.0",
+ "multimediaviewer-license-cc-by-sa-2.1": "CC BY-SA 2.1",
+ "multimediaviewer-license-cc-by-sa-2.5": "CC BY-SA 2.5",
+ "multimediaviewer-license-cc-by-sa-3.0": "CC BY-SA 3.0",
+ "multimediaviewer-license-cc-by-sa-4.0": "CC BY-SA 4.0",
+ "multimediaviewer-license-cc-pd": "公有領域",
+ "multimediaviewer-license-pd": "公有領域",
+ "multimediaviewer-license-default": "檢視授權條款",
+ "multimediaviewer-permission-title": "權限詳細資料",
+ "multimediaviewer-permission-link": "檢視條款",
+ "multimediaviewer-permission-link-hide": "隱藏條款",
+ "multimediaviewer-permission-viewmore": "檢視更多",
+ "multimediaviewer-about-mmv": "關於",
+ "multimediaviewer-discuss-mmv": "討論",
+ "multimediaviewer-help-mmv": "說明",
+ "multimediaviewer-optout-mmv": "停用媒體檢視器",
+ "multimediaviewer-optin-mmv": "開啟媒體檢視器",
+ "multimediaviewer-optout-pending-mmv": "正在停用媒體檢視器",
+ "multimediaviewer-optin-pending-mmv": "正在開啟媒體檢視器",
+ "multimediaviewer-optout-help": "將不再使用媒體檢視器來顯示圖片。 若要再使用媒體檢視器,請點選圖片旁的 \"{{int:multimediaviewer-view-expanded}}\" 按鈕,然後再點選 \"{{int:multimediaviewer-optin-mmv}}\"。",
+ "multimediaviewer-optin-help": "將使用媒體檢視器來顯示圖片。",
+ "multimediaviewer-geolocation": "位置:$1",
+ "multimediaviewer-reuse-link": "分享或內嵌此檔案",
+ "multimediaviewer-reuse-loading-placeholder": "讀取中…",
+ "multimediaviewer-share-tab": "分享",
+ "multimediaviewer-embed-tab": "內嵌",
+ "multimediaviewer-download-link": "下載此檔案",
+ "multimediaviewer-download-preview-link-title": "在瀏覽器中檢視",
+ "multimediaviewer-download-original-button-name": "下載原始檔案",
+ "multimediaviewer-download-small-button-name": "下載小型尺寸",
+ "multimediaviewer-download-medium-button-name": "下載中型尺寸",
+ "multimediaviewer-download-large-button-name": "下載大型尺寸",
+ "multimediaviewer-link-to-page": "連結至檔案描述頁面",
+ "multimediaviewer-link-to-file": "連結至原始檔案",
+ "multimediaviewer-share-explanation": "複製並自由的分享該連結",
+ "multimediaviewer-embed-wt": "Wikitext",
+ "multimediaviewer-embed-html": "HTML",
+ "multimediaviewer-embed-explanation": "使用此程式碼內嵌該檔案",
+ "multimediaviewer-text-embed-credit-text-bl": "由 $1, $2, $3",
+ "multimediaviewer-text-embed-credit-text-b": "由 $1, $2",
+ "multimediaviewer-html-embed-credit-text-bl": "由 $1, $2, $3",
+ "multimediaviewer-html-embed-credit-text-b": "由 $1, $2",
+ "multimediaviewer-html-embed-credit-link-text": "連結",
+ "multimediaviewer-embed-byline": "由 $1",
+ "multimediaviewer-embed-license": "使用 $1 條款授權。",
+ "multimediaviewer-embed-via": "來自 $1。",
+ "multimediaviewer-default-embed-dimensions": "預設縮圖大小",
+ "multimediaviewer-original-embed-dimensions": "原始檔案 $1",
+ "multimediaviewer-large-embed-dimensions": "大型 $1",
+ "multimediaviewer-medium-embed-dimensions": "中型 $1",
+ "multimediaviewer-small-embed-dimensions": "小型 $1",
+ "multimediaviewer-description-page-button-text": "更多有關此檔案的詳細資料",
+ "multimediaviewer-description-page-popup-text": "更多 $1 上有關此檔案的詳細資料",
+ "multimediaviewer-commons-subtitle": "自由的媒體存儲庫",
+ "multimediaviewer-view-expanded": "使用媒體檢視器開啟",
+ "multimediaviewer-view-config": "設定",
+ "multimediaviewer-close-popup-text": "關閉此工具 (Esc)",
+ "multimediaviewer-fullscreen-popup-text": "全螢幕顯示",
+ "multimediaviewer-defullscreen-popup-text": "退出全螢幕",
+ "multimediaviewer-next-image-alt-text": "顯示下張圖片",
+ "multimediaviewer-prev-image-alt-text": "顯示上張圖片",
+ "multimediaviewer-title-popup-text": "描述說明",
+ "multimediaviewer-credit-popup-text": "作者與來源資訊",
+ "multimediaviewer-title-popup-text-more": "點選以顯示完整描述說明",
+ "multimediaviewer-credit-popup-text-more": "檢視完整作者與來源",
+ "multimediaviewer-download-attribution-cta-header": "您需要註明作者",
+ "multimediaviewer-download-optional-attribution-cta-header": "您可以註明作者",
+ "multimediaviewer-download-attribution-cta": "教我怎麼做",
+ "multimediaviewer-download-attribution-copy": "選擇並複製 (若支援) 此檔案的署名文字",
+ "multimediaviewer-attr-plain": "純文字",
+ "multimediaviewer-options-tooltip": "開啟或關閉媒體檢視器",
+ "multimediaviewer-options-dialog-header": "關閉媒體檢視器?",
+ "multimediaviewer-options-text-header": "對所有檔案略過此檢視功能。",
+ "multimediaviewer-options-text-body": "您可稍後在檔案明細頁面開啟。",
+ "multimediaviewer-options-learn-more": "瞭解更多",
+ "multimediaviewer-option-submit-button": "關閉媒體檢視器",
+ "multimediaviewer-option-cancel-button": "取消",
+ "multimediaviewer-disable-confirmation-header": "您已關閉媒體檢視器",
+ "multimediaviewer-disable-confirmation-text": "下次您點選 $1 上的縮圖時,您會直接檢視所有檔案明細。",
+ "multimediaviewer-enable-dialog-header": "開啟媒體檢視器?",
+ "multimediaviewer-enable-text-header": "預設對所有檔案開啟此媒體檢視功能。",
+ "multimediaviewer-enable-submit-button": "開啟媒體檢視器",
+ "multimediaviewer-enable-confirmation-header": "您已對所有檔案開啟媒體檢視器",
+ "multimediaviewer-enable-confirmation-text": "下次您點選 $1 上的縮圖時,會使用媒體檢視器。",
+ "multimediaviewer-enable-alert": "媒體檢視器現在已關閉",
+ "multimediaviewer-disable-info-title": "您已關閉媒體檢視器",
+ "multimediaviewer-disable-info": "您仍可以使用媒體檢視器來檢視個別檔案。"
+}
diff --git a/www/wiki/extensions/MultimediaViewer/importml.sh b/www/wiki/extensions/MultimediaViewer/importml.sh
new file mode 100755
index 00000000..148b5907
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/importml.sh
@@ -0,0 +1,25 @@
+#!/usr/bin/env bash
+
+if [ $1 = "--reverse" ]; then
+ MLDIR=$2
+else
+ MLDIR=$1
+fi
+
+JSDIR=$MLDIR/lib
+CSSDIR=$MLDIR/css
+IMGDIR=$MLDIR/img
+HOOKSFILE=$MLDIR/hooks.txt
+LOCALMLDIR=resources/multilightbox
+
+if [ $1 = "--reverse" ]; then
+ cp $LOCALMLDIR/*.js $JSDIR
+ cp $LOCALMLDIR/multilightbox.css $CSSDIR
+ cp img/close.svg img/fullscreen.svg img/defullscreen.svg $IMGDIR
+ cp $LOCALMLDIR/hooks.txt $HOOKSFILE
+else
+ cp $JSDIR/* resources/multilightbox/
+ cp $CSSDIR/* resources/multilightbox/
+ cp $IMGDIR/* img/
+ cp $HOOKSFILE resources/multilightbox/
+fi
diff --git a/www/wiki/extensions/MultimediaViewer/includes/MultimediaViewerHooks.php b/www/wiki/extensions/MultimediaViewer/includes/MultimediaViewerHooks.php
new file mode 100644
index 00000000..80a18816
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/includes/MultimediaViewerHooks.php
@@ -0,0 +1,349 @@
+<?php
+/**
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @file
+ * @ingroup extensions
+ * @author Mark Holmquist <mtraceur@member.fsf.org>
+ * @copyright Copyright © 2013, Mark Holmquist
+ */
+
+class MultimediaViewerHooks {
+ /** Link to more information about this module */
+ protected static $infoLink =
+ 'https://mediawiki.org/wiki/Special:MyLanguage/Extension:Media_Viewer/About';
+
+ /** Link to a page where this module can be discussed */
+ protected static $discussionLink =
+ 'https://mediawiki.org/wiki/Special:MyLanguage/Extension_talk:Media_Viewer/About';
+
+ /** Link to help about this module */
+ protected static $helpLink =
+ 'https://mediawiki.org/wiki/Special:MyLanguage/Help:Extension:Media_Viewer';
+
+ public static function onUserGetDefaultOptions( &$defaultOptions ) {
+ global $wgMediaViewerEnableByDefault;
+
+ if ( $wgMediaViewerEnableByDefault ) {
+ $defaultOptions['multimediaviewer-enable'] = 1;
+ }
+
+ return true;
+ }
+
+ public static function onExtensionFunctions() {
+ global $wgResourceModules;
+
+ if ( isset( $wgResourceModules['ext.eventLogging'] ) ) {
+ $wgResourceModules['mmv.lightboxinterface']['dependencies'][] = 'ext.eventLogging';
+ $wgResourceModules['mmv']['dependencies'][] = 'ext.eventLogging';
+ $wgResourceModules['mmv.bootstrap.autostart']['dependencies'][] = 'ext.eventLogging';
+ }
+ }
+
+ public static function onEventLoggingRegisterSchemas( array &$schemas ) {
+ $schemas += [
+ 'MediaViewer' => 10867062,
+ 'MultimediaViewerNetworkPerformance' => 15573630,
+ 'MultimediaViewerDuration' => 10427980,
+ 'MultimediaViewerAttribution' => 9758179,
+ 'MultimediaViewerDimensions' => 10014238,
+ ];
+ }
+
+ /**
+ * Checks the context for whether to load the viewer.
+ * @param User $user
+ * @return bool
+ */
+ protected static function shouldHandleClicks( $user ) {
+ global $wgMediaViewerIsInBeta, $wgMediaViewerEnableByDefaultForAnonymous,
+ $wgMediaViewerEnableByDefault;
+
+ if ( $wgMediaViewerIsInBeta && class_exists( 'BetaFeatures' ) ) {
+ return BetaFeatures::isFeatureEnabled( $user, 'multimedia-viewer' );
+ }
+
+ if ( $wgMediaViewerEnableByDefaultForAnonymous === null ) {
+ $enableByDefaultForAnons = $wgMediaViewerEnableByDefault;
+ } else {
+ $enableByDefaultForAnons = $wgMediaViewerEnableByDefaultForAnonymous;
+ }
+
+ if ( !$user->isLoggedIn() ) {
+ return (bool)$enableByDefaultForAnons;
+ } else {
+ return (bool)$user->getOption( 'multimediaviewer-enable' );
+ }
+ }
+
+ /**
+ * Handler for all places where we add the modules
+ * Could be on article pages or on Category pages
+ * @param OutputPage &$out
+ * @return bool
+ */
+ protected static function getModules( &$out ) {
+ $out->addModules( [ 'mmv.head', 'mmv.bootstrap.autostart' ] );
+
+ return true;
+ }
+
+ /**
+ * Handler for BeforePageDisplay hook
+ * Add JavaScript to the page when an image is on it
+ * and the user has enabled the feature if BetaFeatures is installed
+ * @param OutputPage &$out
+ * @param Skin &$skin
+ * @return bool
+ */
+ public static function getModulesForArticle( &$out, &$skin ) {
+ $pageHasThumbnails = count( $out->getFileSearchOptions() ) > 0;
+ $pageIsFilePage = $out->getTitle()->inNamespace( NS_FILE );
+ $fileRelatedSpecialPages = [ 'NewFiles', 'ListFiles', 'MostLinkedFiles',
+ 'MostGloballyLinkedFiles', 'UncategorizedFiles', 'UnusedFiles', 'Search' ];
+ $pageIsFileRelatedSpecialPage = $out->getTitle()->inNamespace( NS_SPECIAL )
+ && in_array( $out->getTitle()->getText(), $fileRelatedSpecialPages );
+
+ if ( $pageHasThumbnails || $pageIsFilePage || $pageIsFileRelatedSpecialPage ) {
+ return self::getModules( $out );
+ }
+
+ return true;
+ }
+
+ /**
+ * Handler for CategoryPageView hook
+ * Add JavaScript to the page if there are images in the category
+ * @param CategoryPage &$catPage
+ * @return bool
+ */
+ public static function getModulesForCategory( &$catPage ) {
+ $title = $catPage->getTitle();
+ $cat = Category::newFromTitle( $title );
+ if ( $cat->getFileCount() > 0 ) {
+ $out = $catPage->getContext()->getOutput();
+ return self::getModules( $out );
+ }
+
+ return true;
+ }
+
+ /**
+ * Add a beta preference to gate the feature
+ * @param User $user
+ * @param array &$prefs
+ * @return true
+ */
+ public static function getBetaPreferences( $user, &$prefs ) {
+ global $wgExtensionAssetsPath, $wgMediaViewerIsInBeta;
+
+ if ( !$wgMediaViewerIsInBeta ) {
+ return true;
+ }
+
+ $prefs['multimedia-viewer'] = [
+ 'label-message' => 'multimediaviewer-pref',
+ 'desc-message' => 'multimediaviewer-pref-desc',
+ 'info-link' => self::$infoLink,
+ 'discussion-link' => self::$discussionLink,
+ 'help-link' => self::$helpLink,
+ 'screenshot' => [
+ 'ltr' => "$wgExtensionAssetsPath/MultimediaViewer/viewer-ltr.svg",
+ 'rtl' => "$wgExtensionAssetsPath/MultimediaViewer/viewer-rtl.svg",
+ ],
+ ];
+
+ return true;
+ }
+
+ /**
+ * Adds a default-enabled preference to gate the feature on non-beta sites
+ * @param User $user
+ * @param array &$prefs
+ * @return true
+ */
+ public static function getPreferences( $user, &$prefs ) {
+ global $wgMediaViewerIsInBeta;
+
+ if ( !$wgMediaViewerIsInBeta ) {
+ $prefs['multimediaviewer-enable'] = [
+ 'type' => 'toggle',
+ 'label-message' => 'multimediaviewer-optin-pref',
+ 'section' => 'rendering/files',
+ ];
+ }
+
+ return true;
+ }
+
+ /**
+ * Export variables used in both PHP and JS to keep DRY
+ * @param array &$vars
+ * @return bool
+ */
+ public static function resourceLoaderGetConfigVars( &$vars ) {
+ global $wgMediaViewerActionLoggingSamplingFactorMap,
+ $wgMediaViewerNetworkPerformanceSamplingFactor,
+ $wgMediaViewerDurationLoggingSamplingFactor,
+ $wgMediaViewerDurationLoggingLoggedinSamplingFactor,
+ $wgMediaViewerAttributionLoggingSamplingFactor,
+ $wgMediaViewerDimensionLoggingSamplingFactor,
+ $wgMediaViewerIsInBeta, $wgMediaViewerUseThumbnailGuessing, $wgMediaViewerExtensions,
+ $wgMediaViewerImageQueryParameter, $wgMediaViewerRecordVirtualViewBeaconURI;
+
+ $vars['wgMultimediaViewer'] = [
+ 'infoLink' => self::$infoLink,
+ 'discussionLink' => self::$discussionLink,
+ 'helpLink' => self::$helpLink,
+ 'useThumbnailGuessing' => (bool)$wgMediaViewerUseThumbnailGuessing,
+ 'durationSamplingFactor' => $wgMediaViewerDurationLoggingSamplingFactor,
+ 'durationSamplingFactorLoggedin' => $wgMediaViewerDurationLoggingLoggedinSamplingFactor,
+ 'networkPerformanceSamplingFactor' => $wgMediaViewerNetworkPerformanceSamplingFactor,
+ 'actionLoggingSamplingFactorMap' => $wgMediaViewerActionLoggingSamplingFactorMap,
+ 'attributionSamplingFactor' => $wgMediaViewerAttributionLoggingSamplingFactor,
+ 'dimensionSamplingFactor' => $wgMediaViewerDimensionLoggingSamplingFactor,
+ 'imageQueryParameter' => $wgMediaViewerImageQueryParameter,
+ 'recordVirtualViewBeaconURI' => $wgMediaViewerRecordVirtualViewBeaconURI,
+ 'tooltipDelay' => 1000,
+ 'extensions' => $wgMediaViewerExtensions,
+ ];
+ $vars['wgMediaViewer'] = true;
+ $vars['wgMediaViewerIsInBeta'] = $wgMediaViewerIsInBeta;
+
+ return true;
+ }
+
+ /**
+ * Export variables which depend on the current user
+ * @param array &$vars
+ * @param OutputPage $out
+ */
+ public static function makeGlobalVariablesScript( &$vars, OutputPage $out ) {
+ $defaultUserOptions = User::getDefaultOptions();
+
+ $user = $out->getUser();
+ $vars['wgMediaViewerOnClick'] = self::shouldHandleClicks( $user );
+ // needed because of bug 69942; could be different for anon and logged-in
+ $vars['wgMediaViewerEnabledByDefault'] =
+ !empty( $defaultUserOptions['multimediaviewer-enable'] );
+ }
+
+ /**
+ * Get modules for testing our JavaScript
+ * @param array &$testModules
+ * @param ResourceLoader &$resourceLoader
+ * @return bool
+ */
+ public static function getTestModules( array &$testModules, ResourceLoader &$resourceLoader ) {
+ $testModules['qunit']['mmv.tests'] = [
+ 'scripts' => [
+ 'tests/qunit/mmv/mmv.bootstrap.test.js',
+ 'tests/qunit/mmv/mmv.test.js',
+ 'tests/qunit/mmv/mmv.lightboxinterface.test.js',
+ 'tests/qunit/mmv/mmv.lightboximage.test.js',
+ 'tests/qunit/mmv/mmv.ThumbnailWidthCalculator.test.js',
+ 'tests/qunit/mmv/mmv.EmbedFileFormatter.test.js',
+ 'tests/qunit/mmv/mmv.Config.test.js',
+ 'tests/qunit/mmv/mmv.HtmlUtils.test.js',
+ 'tests/qunit/mmv/logging/mmv.logging.DurationLogger.test.js',
+ 'tests/qunit/mmv/logging/mmv.logging.PerformanceLogger.test.js',
+ 'tests/qunit/mmv/logging/mmv.logging.ActionLogger.test.js',
+ 'tests/qunit/mmv/logging/mmv.logging.AttributionLogger.test.js',
+ 'tests/qunit/mmv/logging/mmv.logging.DimensionLogger.test.js',
+ 'tests/qunit/mmv/logging/mmv.logging.ViewLogger.test.js',
+ 'tests/qunit/mmv/model/mmv.model.test.js',
+ 'tests/qunit/mmv/model/mmv.model.IwTitle.test.js',
+ 'tests/qunit/mmv/model/mmv.model.TaskQueue.test.js',
+ 'tests/qunit/mmv/model/mmv.model.License.test.js',
+ 'tests/qunit/mmv/model/mmv.model.Image.test.js',
+ 'tests/qunit/mmv/model/mmv.model.Repo.test.js',
+ 'tests/qunit/mmv/model/mmv.model.EmbedFileInfo.test.js',
+ 'tests/qunit/mmv/provider/mmv.provider.Api.test.js',
+ 'tests/qunit/mmv/provider/mmv.provider.ImageInfo.test.js',
+ 'tests/qunit/mmv/provider/mmv.provider.FileRepoInfo.test.js',
+ 'tests/qunit/mmv/provider/mmv.provider.ThumbnailInfo.test.js',
+ 'tests/qunit/mmv/provider/mmv.provider.GuessedThumbnailInfo.test.js',
+ 'tests/qunit/mmv/provider/mmv.provider.Image.test.js',
+ 'tests/qunit/mmv/routing/mmv.routing.MainFileRoute.test.js',
+ 'tests/qunit/mmv/routing/mmv.routing.ThumbnailRoute.test.js',
+ 'tests/qunit/mmv/routing/mmv.routing.Router.test.js',
+ 'tests/qunit/mmv/ui/mmv.ui.test.js',
+ 'tests/qunit/mmv/ui/mmv.ui.canvas.test.js',
+ 'tests/qunit/mmv/ui/mmv.ui.canvasButtons.test.js',
+ 'tests/qunit/mmv/ui/mmv.ui.description.test.js',
+ 'tests/qunit/mmv/ui/mmv.ui.download.pane.test.js',
+ 'tests/qunit/mmv/ui/mmv.ui.metadataPanel.test.js',
+ 'tests/qunit/mmv/ui/mmv.ui.metadataPanelScroller.test.js',
+ 'tests/qunit/mmv/ui/mmv.ui.progressBar.test.js',
+ 'tests/qunit/mmv/ui/mmv.ui.permission.test.js',
+ 'tests/qunit/mmv/ui/mmv.ui.stripeButtons.test.js',
+ 'tests/qunit/mmv/ui/mmv.ui.reuse.dialog.test.js',
+ 'tests/qunit/mmv/ui/mmv.ui.reuse.embed.test.js',
+ 'tests/qunit/mmv/ui/mmv.ui.reuse.share.test.js',
+ 'tests/qunit/mmv/ui/mmv.ui.reuse.tab.test.js',
+ 'tests/qunit/mmv/ui/mmv.ui.reuse.utils.test.js',
+ 'tests/qunit/mmv/ui/mmv.ui.tipsyDialog.test.js',
+ 'tests/qunit/mmv/ui/mmv.ui.truncatableTextField.test.js',
+ 'tests/qunit/mmv/ui/mmv.ui.viewingOptions.test.js',
+ 'tests/qunit/mmv/mmv.testhelpers.js',
+ ],
+ 'dependencies' => [
+ 'mmv.head',
+ 'mmv.bootstrap',
+ 'mmv',
+ 'mmv.ui.ondemandshareddependencies',
+ 'mmv.ui.reuse.shareembed',
+ 'mmv.ui.download.pane',
+ 'mmv.ui.tipsyDialog',
+ 'moment',
+ ],
+ 'localBasePath' => dirname( __DIR__ ),
+ 'remoteExtPath' => 'MultimediaViewer',
+ ];
+
+ return true;
+ }
+
+ /**
+ * Modify thumbnail DOM
+ * @param ThumbnailImage $thumbnail
+ * @param array &$attribs Attributes of the <img> element
+ * @param array|bool &$linkAttribs Attributes of the wrapping <a> element
+ * @return true
+ */
+ public static function thumbnailBeforeProduceHTML( ThumbnailImage $thumbnail, array &$attribs,
+ &$linkAttribs
+ ) {
+ $file = $thumbnail->getFile();
+
+ if ( $file ) {
+ // At the moment all classes that extend File have getWidth() and getHeight()
+ // but since the File class doesn't have these methods defined, this check
+ // is more future-proof
+
+ if ( method_exists( $file, 'getWidth' ) ) {
+ $attribs['data-file-width'] = $file->getWidth();
+ }
+
+ if ( method_exists( $file, 'getHeight' ) ) {
+ $attribs['data-file-height'] = $file->getHeight();
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/www/wiki/extensions/MultimediaViewer/jsduck.categories.json b/www/wiki/extensions/MultimediaViewer/jsduck.categories.json
new file mode 100644
index 00000000..b994017e
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/jsduck.categories.json
@@ -0,0 +1,104 @@
+[
+ {
+ "name": "Media Viewer classes",
+ "groups": [
+ {
+ "name": "Base",
+ "classes": [
+ "mw.mmv.Config",
+ "mw.mmv.EmbedFileFormatter",
+ "mw.mmv.HtmlUtils",
+ "mw.mmv.LightboxImage",
+ "mw.mmv.LightboxInterface",
+ "mw.mmv.MultimediaViewer",
+ "mw.mmv.MultimediaViewerBootstrap",
+ "mw.mmv.ThumbnailWidthCalculator"
+ ]
+ },
+ {
+ "name": "Loggers",
+ "classes": [
+ "mw.mmv.logging.*"
+ ]
+ },
+ {
+ "name": "Models",
+ "classes": [
+ "mw.mmv.model.*"
+ ]
+ },
+ {
+ "name": "Providers",
+ "classes": [
+ "mw.mmv.provider.*"
+ ]
+ },
+ {
+ "name": "Routers",
+ "classes": [
+ "mw.mmv.routing.*"
+ ]
+ },
+ {
+ "name": "Interface",
+ "classes": [
+ "mw.mmv.ui.*"
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "External",
+ "groups": [
+ {
+ "name": "jQuery",
+ "classes": [
+ "jQuery",
+ "jQuery.Promise",
+ "jQuery.Deferred",
+ "jQuery.Event",
+ "jqXHR"
+ ]
+ },
+ {
+ "name": "JavaScript natives",
+ "classes": [
+ "Array",
+ "Boolean",
+ "Date",
+ "Function",
+ "Number",
+ "Object",
+ "RegExp",
+ "String"
+ ]
+ },
+ {
+ "name": "OOUI",
+ "classes": [
+ "OO.ui.*"
+ ]
+ },
+ {
+ "name": "MediaWiki",
+ "classes": [
+ "mw",
+ "mw.Api",
+ "mw.Title",
+ "mw.Map",
+ "mw.storage",
+ "mw.eventLog"
+ ]
+ },
+ {
+ "name": "Browser native classes",
+ "classes": [
+ "HTMLElement",
+ "HTMLImageElement",
+ "XMLHttpRequest"
+ ]
+ }
+ ]
+ }
+]
diff --git a/www/wiki/extensions/MultimediaViewer/jsduck.external.js b/www/wiki/extensions/MultimediaViewer/jsduck.external.js
new file mode 100644
index 00000000..45c713db
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/jsduck.external.js
@@ -0,0 +1,87 @@
+/**
+ * @class mw
+ * <https://doc.wikimedia.org/mediawiki-core/master/js/#!/api/mw>
+ * @singleton
+ */
+
+/**
+ * @class mw.Api
+ * <https://doc.wikimedia.org/mediawiki-core/master/js/#!/api/mw.Api>
+ */
+
+/**
+ * @class mw.Title
+ * <https://doc.wikimedia.org/mediawiki-core/master/js/#!/api/mw.Title>
+ */
+
+/**
+ * @class mw.eventLog
+ * <https://www.mediawiki.org/wiki/Extension:EventLogging>
+ */
+
+/**
+ * @class mw.Map
+ * Associative array which is used for various configuration objects, most prominently mw.config:
+ * <https://www.mediawiki.org/wiki/Manual:Interface/JavaScript#mw.config>
+ */
+
+/**
+ * @class mw.storage
+ * <https://doc.wikimedia.org/mediawiki-core/master/js/#!/api/mw.storage>
+ */
+
+/**
+ * @class HTMLElement
+ * An HTML element.
+ */
+
+/**
+ * @class HTMLImageElement
+ * @extends HTMLElement
+ * An HTML <img> element.
+ */
+
+/**
+ * @class OO.ui.MenuOptionWidget
+ * <https://doc.wikimedia.org/mediawiki-core/master/js/#!/api/OO.ui.MenuOptionWidget>
+ */
+
+/**
+ * @class OO.ui.MenuSelectWidget
+ * <https://doc.wikimedia.org/mediawiki-core/master/js/#!/api/OO.ui.MenuSelectWidget>
+ */
+
+/**
+ * @class OO.ui.DropdownWidget
+ * <https://doc.wikimedia.org/mediawiki-core/master/js/#!/api/OO.ui.DropdownWidget>
+ */
+
+/**
+ * @class XMLHttpRequest
+ * An AJAX request
+ */
+
+/**
+ * @class jQuery
+ * A jQuery object.
+ */
+
+/**
+ * @class jQuery.Promise
+ * A jQuery promise object.
+ */
+
+/**
+ * @class jQuery.Deferred
+ * A jQuery deferred object.
+ */
+
+/**
+ * @class jQuery.Event
+ * An event object with extra jQuery data.
+ */
+
+/**
+ * @class jqXHR
+ * An XMLHttpRequest object wrapped by jQuery
+ */
diff --git a/www/wiki/extensions/MultimediaViewer/jsduck.json b/www/wiki/extensions/MultimediaViewer/jsduck.json
new file mode 100644
index 00000000..a9be81f9
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/jsduck.json
@@ -0,0 +1,12 @@
+{
+ "--title": "MultimediaViewer - Documentation",
+ "--categories": "jsduck.categories.json",
+ "--builtin-classes": true,
+ "--processes": "0",
+ "--warnings-exit-nonzero": true,
+ "--output": "docs",
+ "--": [
+ "jsduck.external.js",
+ "resources/mmv"
+ ]
+}
diff --git a/www/wiki/extensions/MultimediaViewer/package.json b/www/wiki/extensions/MultimediaViewer/package.json
new file mode 100644
index 00000000..e54ceb9b
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/package.json
@@ -0,0 +1,18 @@
+{
+ "private": true,
+ "scripts": {
+ "test": "grunt test",
+ "doc": "jsduck"
+ },
+ "devDependencies": {
+ "eslint-config-wikimedia": "0.5.0",
+ "grunt": "1.0.1",
+ "grunt-banana-checker": "0.6.0",
+ "grunt-eslint": "20.0.0",
+ "grunt-jsonlint": "1.1.0",
+ "grunt-stylelint": "0.9.0",
+ "grunt-svgmin": "5.0.0",
+ "stylelint": "8.2.0",
+ "stylelint-config-wikimedia": "0.4.2"
+ }
+}
diff --git a/www/wiki/extensions/MultimediaViewer/resources/jquery.hashchange/jquery.hashchange.js b/www/wiki/extensions/MultimediaViewer/resources/jquery.hashchange/jquery.hashchange.js
new file mode 100644
index 00000000..b7e7db31
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/jquery.hashchange/jquery.hashchange.js
@@ -0,0 +1,260 @@
+/*!
+ * jQuery hashchange event - v1.3 - 7/21/2010
+ * http://benalman.com/projects/jquery-hashchange-plugin/
+ *
+ * Copyright (c) 2010 "Cowboy" Ben Alman
+ * Dual licensed under the MIT and GPL licenses.
+ * http://benalman.com/about/license/
+ */
+
+// Forked on July 10th 2014 by Gilles Dubuc (WMF) in order to add jQuery 1.9 compatibility
+// The fork was done because upstream is currently unmaintained and not responding to merge requests
+
+// Script: jQuery hashchange event
+//
+// *Version: 1.3, Last updated: 7/21/2010*
+//
+// Project Home - http://benalman.com/projects/jquery-hashchange-plugin/
+// GitHub - http://github.com/cowboy/jquery-hashchange/
+// Source - http://github.com/cowboy/jquery-hashchange/raw/master/jquery.ba-hashchange.js
+// (Minified) - http://github.com/cowboy/jquery-hashchange/raw/master/jquery.ba-hashchange.min.js (0.8kb gzipped)
+//
+// About: License
+//
+// Copyright (c) 2010 "Cowboy" Ben Alman,
+// Dual licensed under the MIT and GPL licenses.
+// http://benalman.com/about/license/
+//
+// About: Examples
+//
+// These working examples, complete with fully commented code, illustrate a few
+// ways in which this plugin can be used.
+//
+// hashchange event - http://benalman.com/code/projects/jquery-hashchange/examples/hashchange/
+// document.domain - http://benalman.com/code/projects/jquery-hashchange/examples/document_domain/
+//
+// About: Support and Testing
+//
+// Information about what version or versions of jQuery this plugin has been
+// tested with, what browsers it has been tested in, and where the unit tests
+// reside (so you can test it yourself).
+//
+// jQuery Versions - 1.2.6, 1.3.2, 1.4.1, 1.4.2
+// Browsers Tested - Internet Explorer 6-8, Firefox 2-4, Chrome 5-6, Safari 3.2-5,
+// Opera 9.6-10.60, iPhone 3.1, Android 1.6-2.2, BlackBerry 4.6-5.
+// Unit Tests - http://benalman.com/code/projects/jquery-hashchange/unit/
+//
+// About: Known issues
+//
+// While this jQuery hashchange event implementation is quite stable and
+// robust, there are a few unfortunate browser bugs surrounding expected
+// hashchange event-based behaviors, independent of any JavaScript
+// window.onhashchange abstraction. See the following examples for more
+// information:
+//
+// Chrome: Back Button - http://benalman.com/code/projects/jquery-hashchange/examples/bug-chrome-back-button/
+// Firefox: Remote XMLHttpRequest - http://benalman.com/code/projects/jquery-hashchange/examples/bug-firefox-remote-xhr/
+// WebKit: Back Button in an Iframe - http://benalman.com/code/projects/jquery-hashchange/examples/bug-webkit-hash-iframe/
+// Safari: Back Button from a different domain - http://benalman.com/code/projects/jquery-hashchange/examples/bug-safari-back-from-diff-domain/
+//
+// Also note that should a browser natively support the window.onhashchange
+// event, but not report that it does, the fallback polling loop will be used.
+//
+// About: Release History
+//
+// 1.3 - (7/21/2010) Reorganized IE6/7 Iframe code to make it more
+// "removable" for mobile-only development. Added IE6/7 document.title
+// support. Attempted to make Iframe as hidden as possible by using
+// techniques from http://www.paciellogroup.com/blog/?p=604. Added
+// support for the "shortcut" format $(window).hashchange( fn ) and
+// $(window).hashchange() like jQuery provides for built-in events.
+// Renamed jQuery.hashchangeDelay to <jQuery.fn.hashchange.delay> and
+// lowered its default value to 50. Added <jQuery.fn.hashchange.domain>
+// and <jQuery.fn.hashchange.src> properties plus document-domain.html
+// file to address access denied issues when setting document.domain in
+// IE6/7.
+// 1.2 - (2/11/2010) Fixed a bug where coming back to a page using this plugin
+// from a page on another domain would cause an error in Safari 4. Also,
+// IE6/7 Iframe is now inserted after the body (this actually works),
+// which prevents the page from scrolling when the event is first bound.
+// Event can also now be bound before DOM ready, but it won't be usable
+// before then in IE6/7.
+// 1.1 - (1/21/2010) Incorporated document.documentMode test to fix IE8 bug
+// where browser version is incorrectly reported as 8.0, despite
+// inclusion of the X-UA-Compatible IE=EmulateIE7 meta tag.
+// 1.0 - (1/9/2010) Initial Release. Broke out the jQuery BBQ event.special
+// window.onhashchange functionality into a separate plugin for users
+// who want just the basic event & back button support, without all the
+// extra awesomeness that BBQ provides. This plugin will be included as
+// part of jQuery BBQ, but also be available separately.
+
+(function($,window,undefined){
+ '$:nomunge'; // Used by YUI compressor.
+
+ // Reused string.
+ var str_hashchange = 'hashchange',
+
+ // Method / object references.
+ doc = document,
+ fake_onhashchange,
+ special = $.event.special,
+
+ // Does the browser support window.onhashchange?
+ supports_onhashchange = 'on' + str_hashchange in window;
+
+ // Get location.hash (or what you'd expect location.hash to be) sans any
+ // leading #. Thanks for making this necessary, Firefox!
+ function get_fragment( url ) {
+ url = url || location.href;
+ return '#' + url.replace( /^[^#]*#?(.*)$/, '$1' );
+ };
+
+ // Method: jQuery.fn.hashchange
+ //
+ // Bind a handler to the window.onhashchange event or trigger all bound
+ // window.onhashchange event handlers. This behavior is consistent with
+ // jQuery's built-in event handlers.
+ //
+ // Usage:
+ //
+ // > jQuery(window).hashchange( [ handler ] );
+ //
+ // Arguments:
+ //
+ // handler - (Function) Optional handler to be bound to the hashchange
+ // event. This is a "shortcut" for the more verbose form:
+ // jQuery(window).bind( 'hashchange', handler ). If handler is omitted,
+ // all bound window.onhashchange event handlers will be triggered. This
+ // is a shortcut for the more verbose
+ // jQuery(window).trigger( 'hashchange' ). These forms are described in
+ // the <hashchange event> section.
+ //
+ // Returns:
+ //
+ // (jQuery) The initial jQuery collection of elements.
+
+ // Allow the "shortcut" format $(elem).hashchange( fn ) for binding and
+ // $(elem).hashchange() for triggering, like jQuery does for built-in events.
+ $.fn[ str_hashchange ] = function( fn ) {
+ return fn ? this.bind( str_hashchange, fn ) : this.trigger( str_hashchange );
+ };
+
+ // Property: jQuery.fn.hashchange.delay
+ //
+ // The numeric interval (in milliseconds) at which the <hashchange event>
+ // polling loop executes. Defaults to 50.
+
+ $.fn[ str_hashchange ].delay = 50;
+
+ // Event: hashchange event
+ //
+ // Fired when location.hash changes. In browsers that support it, the native
+ // HTML5 window.onhashchange event is used, otherwise a polling loop is
+ // initialized, running every <jQuery.fn.hashchange.delay> milliseconds to
+ // see if the hash has changed.
+ //
+ // Usage as described in <jQuery.fn.hashchange>:
+ //
+ // > // Bind an event handler.
+ // > jQuery(window).hashchange( function(e) {
+ // > var hash = location.hash;
+ // > ...
+ // > });
+ // >
+ // > // Manually trigger the event handler.
+ // > jQuery(window).hashchange();
+ //
+ // A more verbose usage that allows for event namespacing:
+ //
+ // > // Bind an event handler.
+ // > jQuery(window).bind( 'hashchange', function(e) {
+ // > var hash = location.hash;
+ // > ...
+ // > });
+ // >
+ // > // Manually trigger the event handler.
+ // > jQuery(window).trigger( 'hashchange' );
+ //
+ // Additional Notes:
+ //
+ // * The polling loop and Iframe are not created until at least one handler
+ // is actually bound to the 'hashchange' event.
+ // * If you need the bound handler(s) to execute immediately, in cases where
+ // a location.hash exists on page load, via bookmark or page refresh for
+ // example, use jQuery(window).hashchange() or the more verbose
+ // jQuery(window).trigger( 'hashchange' ).
+
+ // Override existing $.event.special.hashchange methods (allowing this plugin
+ // to be defined after jQuery BBQ in BBQ's source code).
+ special[ str_hashchange ] = $.extend( special[ str_hashchange ], {
+
+ // Called only when the first 'hashchange' event is bound to window.
+ setup: function() {
+ // If window.onhashchange is supported natively, there's nothing to do..
+ if ( supports_onhashchange ) { return false; }
+
+ // Otherwise, we need to create our own. And we don't want to call this
+ // until the user binds to the event, just in case they never do, since it
+ // will create a polling loop and possibly even a hidden Iframe.
+ $( fake_onhashchange.start );
+ },
+
+ // Called only when the last 'hashchange' event is unbound from window.
+ teardown: function() {
+ // If window.onhashchange is supported natively, there's nothing to do..
+ if ( supports_onhashchange ) { return false; }
+
+ // Otherwise, we need to stop ours (if possible).
+ $( fake_onhashchange.stop );
+ }
+
+ });
+
+ // fake_onhashchange creates a polling loop to watch for hash changes and
+ // triggers the window.onhashchange event for browsers that don't natively
+ // support it.
+ fake_onhashchange = (function(){
+ var self = {},
+ timeout_id,
+
+ // Remember the initial hash so it doesn't get triggered immediately.
+ last_hash = get_fragment(),
+
+ fn_retval = function(val){ return val; },
+ history_set = fn_retval,
+ history_get = fn_retval;
+
+ // Start the polling loop.
+ self.start = function() {
+ timeout_id || poll();
+ };
+
+ // Stop the polling loop.
+ self.stop = function() {
+ timeout_id && clearTimeout( timeout_id );
+ timeout_id = undefined;
+ };
+
+ // This polling loop checks every $.fn.hashchange.delay milliseconds to see
+ // if location.hash has changed, and triggers the 'hashchange' event on
+ // window when necessary.
+ function poll() {
+ var hash = get_fragment(),
+ history_hash = history_get( last_hash );
+
+ if ( hash !== last_hash ) {
+ history_set( last_hash = hash, history_hash );
+
+ $(window).trigger( str_hashchange );
+
+ } else if ( history_hash !== last_hash ) {
+ location.href = location.href.replace( /#.*/, '' ) + history_hash;
+ }
+
+ timeout_id = setTimeout( poll, $.fn[ str_hashchange ].delay );
+ };
+
+ return self;
+ })();
+
+})(jQuery,this);
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/img/down.svg b/www/wiki/extensions/MultimediaViewer/resources/mmv/img/down.svg
new file mode 100644
index 00000000..db304af0
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/img/down.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12">
+ <path fill="#fff" fill-rule="evenodd" d="M2.023 3l3.49 5.953L9 3z" clip-rule="evenodd"/>
+</svg>
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/img/expand.svg b/www/wiki/extensions/MultimediaViewer/resources/mmv/img/expand.svg
new file mode 100644
index 00000000..52be87b9
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/img/expand.svg
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 768">
+ <path d="M851.2 71.6L690.7 232.1l-40.1-40.3-9.6 164.8 164.8-9.3-40.3-40.4L926 146.4l58.5 58.5L997.6 0 792.7 13.1"/>
+ <path d="M769.6 89.3H611.9l70.9 70.8 7.9 7.5m-47.1 234.6l-51.2 3 3-51.2 9.4-164.4 5.8-100.3H26.4V768h883.1V387l-100.9 5.8-165 9.4zM813.9 678H113.6l207.2-270.2 31.5-12.9L548 599.8l105.9-63.2 159.8 140.8.2.6zm95.6-291.9V228l-79.1 78.9 7.8 7.9"/>
+</svg>
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/img/gear.svg b/www/wiki/extensions/MultimediaViewer/resources/mmv/img/gear.svg
new file mode 100644
index 00000000..01c7a43c
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/img/gear.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 768">
+ <path d="M897 454.6V313.4L810.4 299c-6.4-23.3-16-45.7-27.3-65.8l50.5-71.4-99.4-100.2-71.4 50.5c-20.9-11.2-42.5-20.9-65.8-27.3L582.6-1H441.4L427 85.6c-23.3 6.4-45.7 16-65.8 27.3l-71.4-50.5-100.3 99.5 50.5 71.4c-11.2 20.9-20.9 42.5-27.3 66.6L127 313.4v141.2l85.8 14.4c6.4 23.3 16 45.7 27.3 66.6L189.6 607l99.5 99.5 71.4-50.5c20.9 11.2 42.5 20.9 66.6 27.3l14.4 85.8h141.2l14.4-86.6c23.3-6.4 45.7-16 65.8-27.3l71.4 50.5 99.5-99.5-50.5-71.4c11.2-20.9 20.9-42.5 27.3-66.6l86.4-13.6zm-385 77c-81.8 0-147.6-66.6-147.6-147.6 0-81.8 66.6-147.6 147.6-147.6S659.6 302.2 659.6 384 593.8 531.6 512 531.6z"/>
+</svg>
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/img/x_gray.svg b/www/wiki/extensions/MultimediaViewer/resources/mmv/img/x_gray.svg
new file mode 100644
index 00000000..739b911e
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/img/x_gray.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 612 792">
+ <path fill="#72777d" d="M612 179.2L522.8 90 306 306.8 89.2 90 0 179.2 216.8 396 0 612.8 89.2 702 306 485.2 522.8 702l89.2-89.2L395.2 396"/>
+</svg>
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/logging/mmv.logging.ActionLogger.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/logging/mmv.logging.ActionLogger.js
new file mode 100644
index 00000000..9f18f972
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/logging/mmv.logging.ActionLogger.js
@@ -0,0 +1,192 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $, oo ) {
+ var L;
+
+ /**
+ * Writes log entries
+ *
+ * @class mw.mmv.logging.ActionLogger
+ * @extends mw.mmv.logging.Logger
+ * @constructor
+ */
+ function ActionLogger() {}
+
+ oo.inheritClass( ActionLogger, mw.mmv.logging.Logger );
+
+ L = ActionLogger.prototype;
+
+ /**
+ * Sampling factor key-value map.
+ *
+ * The map's keys are the action identifiers and the values are the sampling factor for each action type.
+ * There is a "default" key defined providing a default sampling factor for actions that aren't explicitely
+ * set in the map.
+ * @property {Object.<string, number>}
+ * @static
+ */
+ L.samplingFactorMap = mw.config.get( 'wgMultimediaViewer' ).actionLoggingSamplingFactorMap;
+
+ /**
+ * @override
+ * @inheritdoc
+ */
+ L.schema = 'MediaViewer';
+
+ /**
+ * Possible log actions, and their associated English developer log strings.
+ *
+ * These events are not de-duped. Eg. if the user opens the same site link
+ * in 10 tabs, there will be 10 file-description-page events. If they view the
+ * same image 10 times by hitting the prev/next buttons, there will be 10
+ * image-view events, etc.
+ * @property
+ * @static
+ */
+ L.logActions = {
+ thumbnail: 'User clicked on a thumbnail to open Media Viewer.',
+ enlarge: 'User clicked on an enlarge link to open Media Viewer.',
+ fullscreen: 'User entered fullscreen mode.',
+ defullscreen: 'User exited fullscreen mode.',
+ close: 'User closed Media Viewer.',
+ 'view-original-file': 'User clicked on the direct link to the original file',
+ 'file-description-page': 'User opened the file description page.',
+ 'file-description-page-abovefold': 'User opened the file description page via the above-the-fold button.',
+ 'use-this-file-open': 'User opened the dialog to use this file.',
+ 'image-view': 'User viewed an image.',
+ 'metadata-open': 'User opened the metadata panel.',
+ 'metadata-close': 'User closed the metadata panel.',
+ 'metadata-scroll-open': 'User opened the metadata panel by scrolling.',
+ 'metadata-scroll-close': 'User closed the metadata panel by scrolling.',
+ 'next-image': 'User viewed the next image.',
+ 'prev-image': 'User viewed the previous image.',
+ 'terms-open': 'User opened the usage terms.',
+ 'license-page': 'User opened the license page.',
+ 'author-page': 'User opened the author page.',
+ 'source-page': 'User opened the source page.',
+ 'hash-load': 'User loaded the image via a hash on pageload.',
+ 'history-navigation': 'User navigated with the browser history.',
+ 'optout-loggedin': 'opt-out (via quick link at bottom of metadata panel) by logged-in user',
+ 'optout-anon': 'opt-out by anonymous user',
+ 'optin-loggedin': 'opt-in (via quick link at bottom of metadata panel) by logged-in user',
+ 'optin-anon': 'opt-in by anonymous user',
+ 'about-page': 'User opened the about page.',
+ 'discuss-page': 'User opened the discuss page.',
+ 'help-page': 'User opened the help page.',
+ 'location-page': 'User opened the location page.',
+ 'download-select-menu-original': 'User selected the original size in the download dropdown menu.',
+ 'download-select-menu-small': 'User selected the small size in the download dropdown menu.',
+ 'download-select-menu-medium': 'User selected the medium size in the download dropdown menu.',
+ 'download-select-menu-large': 'User selected the large size in the download dropdown menu.',
+ download: 'User clicked on the button to download a file.',
+ 'download-view-in-browser': 'User clicked on the link to view the image in the browser in the download tab.',
+ 'right-click-image': 'User right-clicked on the image.',
+ 'share-page': 'User opened the link to the current image.',
+ 'share-link-copied': 'User copied the share link.',
+ 'embed-html-copied': 'User copied the HTML embed code.',
+ 'embed-wikitext-copied': 'User copied the wikitext embed code.',
+ 'embed-switched-to-html': 'User switched to the HTML embed code.',
+ 'embed-switched-to-wikitext': 'User switched to the wikitext embed code.',
+ 'embed-select-menu-wikitext-default': 'User switched to the default thumbnail size on wikitext.',
+ 'embed-select-menu-wikitext-small': 'User switched to the small thumbnail size on wikitext.',
+ 'embed-select-menu-wikitext-medium': 'User switched to the medium thumbnail size on wikitext.',
+ 'embed-select-menu-wikitext-large': 'User switched to the large thumbnail size on wikitext.',
+ 'embed-select-menu-html-original': 'User switched to the original thumbnail size on html.',
+ 'embed-select-menu-html-small': 'User switched to the small thumbnail size on html.',
+ 'embed-select-menu-html-medium': 'User switched to the medium thumbnail size on html.',
+ 'embed-select-menu-html-large': 'User switched to the large thumbnail size on html.',
+ 'use-this-file-close': 'User closed the dialog to use this file.',
+ 'download-open': 'User opened the dialog to download this file.',
+ 'download-close': 'User closed the dialog to download this file.',
+ 'options-open': 'User opened the enable/disable dialog.',
+ 'options-close': 'User either canceled an enable/disable action or closed a confirmation window.',
+ 'disable-about-link': 'User clicked on the "Learn more" link in the disable window.',
+ 'enable-about-link': 'User clicked on the "Learn more" link in the enable window.',
+ 'image-unview': 'User stopped looking at the current image.'
+ };
+
+ /**
+ * Logs an action
+ *
+ * @param {string} action The key representing the action
+ * @param {boolean} forceEventLog True if we want the action to be logged regardless of the sampling factor
+ * @return {jQuery.Promise}
+ */
+ L.log = function ( action, forceEventLog ) {
+ var actionText = this.logActions[ action ] || action,
+ self = this;
+
+ if ( this.isEnabled( action ) ) {
+ mw.log( actionText );
+ }
+
+ if ( forceEventLog || self.isInSample( action ) ) {
+ return this.loadDependencies().then( function () {
+ self.eventLog.logEvent( self.schema, {
+ action: action,
+ samplingFactor: self.getActionFactor( action )
+ } );
+
+ return true;
+ } );
+ } else {
+ return $.Deferred().resolve( false );
+ }
+ };
+
+ /**
+ * Returns the sampling factor for a given action
+ *
+ * @param {string} action The key representing the action
+ * @return {number} Sampling factor
+ */
+ L.getActionFactor = function ( action ) {
+ return this.samplingFactorMap[ action ] || this.samplingFactorMap.default;
+ };
+
+ /**
+ * Returns whether or not we should measure this request for this action
+ *
+ * @param {string} action The key representing the action
+ * @return {boolean} True if this request needs to be sampled
+ */
+ L.isInSample = function ( action ) {
+ var factor = this.getActionFactor( action );
+
+ if ( !$.isNumeric( factor ) || factor < 1 ) {
+ return false;
+ }
+ return Math.floor( Math.random() * factor ) === 0;
+ };
+
+ /**
+ * Returns whether logging this event is enabled. This is intended for console logging, which
+ * (in debug mode) should be done even if the request is not being sampled, as long as logging
+ * is enabled for some sample.
+ *
+ * @param {string} action The key representing the action
+ * @return {boolean} True if this logging is enabled
+ */
+ L.isEnabled = function ( action ) {
+ var factor = this.getActionFactor( action );
+ return $.isNumeric( factor ) && factor >= 1;
+ };
+
+ mw.mmv.logging.ActionLogger = ActionLogger;
+ mw.mmv.actionLogger = new ActionLogger();
+}( mediaWiki, jQuery, OO ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/logging/mmv.logging.Api.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/logging/mmv.logging.Api.js
new file mode 100644
index 00000000..a6158d78
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/logging/mmv.logging.Api.js
@@ -0,0 +1,58 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $, oo ) {
+ /**
+ * Runs performance analysis on requests via mw.mmv.logging.PerformanceLogger
+ *
+ * @class mw.mmv.logging.Api
+ * @extends mw.Api
+ * @constructor
+ * @param {string} type The type of the requests to be made through this API.
+ * @param {Object} options See mw.Api#defaultOptions
+ */
+ function Api( type, options ) {
+ mw.Api.call( this, options );
+
+ /** @property {mw.mmv.logging.PerformanceLogger} performance Used to record performance data. */
+ this.performance = new mw.mmv.logging.PerformanceLogger();
+
+ /** @property {string} type Type of requests being sent via this API. */
+ this.type = type;
+ }
+
+ oo.inheritClass( Api, mw.Api );
+
+ /**
+ * Runs an AJAX call to the server.
+ *
+ * @override
+ * @param {Object} parameters
+ * @param {Object} [ajaxOptions]
+ * @return {jQuery.Promise.<Object, jqXHR>} Done: API response data. Fail: Error code.
+ */
+ Api.prototype.ajax = function ( parameters, ajaxOptions ) {
+ var start = $.now(),
+ api = this;
+
+ return mw.Api.prototype.ajax.call( this, parameters, ajaxOptions ).done( function ( result, jqxhr ) {
+ api.performance.recordJQueryEntryDelayed( api.type, $.now() - start, jqxhr );
+ } );
+ };
+
+ mw.mmv.logging.Api = Api;
+}( mediaWiki, jQuery, OO ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/logging/mmv.logging.AttributionLogger.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/logging/mmv.logging.AttributionLogger.js
new file mode 100644
index 00000000..52171977
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/logging/mmv.logging.AttributionLogger.js
@@ -0,0 +1,73 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $, oo ) {
+ var AL;
+
+ /**
+ * Writes EventLogging entries for duration measurements
+ *
+ * @class mw.mmv.logging.AttributionLogger
+ * @extends mw.mmv.logging.Logger
+ * @constructor
+ */
+ function AttributionLogger() {}
+
+ oo.inheritClass( AttributionLogger, mw.mmv.logging.Logger );
+
+ AL = AttributionLogger.prototype;
+
+ /**
+ * @override
+ * @inheritdoc
+ */
+ AL.samplingFactor = mw.config.get( 'wgMultimediaViewer' ).attributionSamplingFactor;
+
+ /**
+ * @override
+ * @inheritdoc
+ */
+ AL.schema = 'MultimediaViewerAttribution';
+
+ /**
+ * Logs attribution data
+ *
+ * @param {mw.mmv.model.Image} image Image data
+ */
+ AL.logAttribution = function ( image ) {
+ var data;
+
+ data = {
+ authorPresent: !!image.author,
+ sourcePresent: !!image.source,
+ licensePresent: !!image.license,
+ loggedIn: !mw.user.isAnon(),
+ samplingFactor: this.samplingFactor
+ };
+
+ if ( this.isEnabled() ) {
+ mw.log( 'author: ' + ( data.authorPresent ? 'present' : 'absent' ) +
+ ', source: ' + ( data.sourcePresent ? 'present' : 'absent' ) +
+ ', license: ' + ( data.licensePresent ? 'present' : 'absent' ) );
+ }
+
+ this.log( data );
+ };
+
+ mw.mmv.logging.AttributionLogger = AttributionLogger;
+ mw.mmv.attributionLogger = new AttributionLogger();
+}( mediaWiki, jQuery, OO ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/logging/mmv.logging.DimensionLogger.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/logging/mmv.logging.DimensionLogger.js
new file mode 100644
index 00000000..82ead14c
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/logging/mmv.logging.DimensionLogger.js
@@ -0,0 +1,81 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $, oo ) {
+ var DL;
+
+ /**
+ * Writes EventLogging entries for size measurements related to thumbnail size selection
+ * (bucket size vs. display size).
+ *
+ * @class mw.mmv.logging.DimensionLogger
+ * @extends mw.mmv.logging.Logger
+ * @constructor
+ */
+ function DimensionLogger() {}
+
+ oo.inheritClass( DimensionLogger, mw.mmv.logging.Logger );
+
+ DL = DimensionLogger.prototype;
+
+ /**
+ * @override
+ * @inheritdoc
+ */
+ DL.samplingFactor = mw.config.get( 'wgMultimediaViewer' ).dimensionSamplingFactor;
+
+ /**
+ * @override
+ * @inheritdoc
+ */
+ DL.schema = 'MultimediaViewerDimensions';
+
+ /**
+ * Logs dimension data.
+ *
+ * @param {mw.mmv.model.ThumbnailWidth} imageWidths Widths of the image that will be displayed
+ * @param {Object} canvasDimensions Canvas width and height in CSS pixels
+ * @param {string} context Reason for requesting the image, one of 'show', 'resize', 'preload'
+ */
+ DL.logDimensions = function ( imageWidths, canvasDimensions, context ) {
+ var data;
+
+ data = {
+ screenWidth: screen.width,
+ screenHeight: screen.height,
+ viewportWidth: $( window ).width(),
+ viewportHeight: $( window ).height(),
+ canvasWidth: canvasDimensions.width,
+ canvasHeight: canvasDimensions.height,
+ devicePixelRatio: $.devicePixelRatio(),
+ imgWidth: imageWidths.cssWidth,
+ imageAspectRatio: imageWidths.cssWidth / imageWidths.cssHeight,
+ thumbWidth: imageWidths.real,
+ context: context,
+ samplingFactor: this.samplingFactor
+ };
+
+ if ( this.isEnabled() ) {
+ mw.log( 'mw.mmw.logger.DimensionLogger', data );
+ }
+
+ this.log( data );
+ };
+
+ mw.mmv.logging.DimensionLogger = DimensionLogger;
+ mw.mmv.dimensionLogger = new DimensionLogger();
+}( mediaWiki, jQuery, OO ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/logging/mmv.logging.DurationLogger.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/logging/mmv.logging.DurationLogger.js
new file mode 100644
index 00000000..d2e0f918
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/logging/mmv.logging.DurationLogger.js
@@ -0,0 +1,162 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $, oo ) {
+ var L;
+
+ /**
+ * Writes EventLogging entries for duration measurements
+ *
+ * @class mw.mmv.logging.DurationLogger
+ * @extends mw.mmv.logging.Logger
+ * @constructor
+ */
+ function DurationLogger() {
+ this.starts = {};
+ this.stops = {};
+ }
+
+ oo.inheritClass( DurationLogger, mw.mmv.logging.Logger );
+
+ L = DurationLogger.prototype;
+
+ /**
+ * @override
+ * @inheritdoc
+ */
+ L.samplingFactor = mw.config.get( 'wgMultimediaViewer' ).durationSamplingFactor;
+
+ // If a sampling factor specific to loggedin users is set and we're logged in, apply it
+ if ( mw.config.get( 'wgMultimediaViewer' ).durationSamplingFactorLoggedin && !mw.user.isAnon() ) {
+ L.samplingFactor = mw.config.get( 'wgMultimediaViewer' ).durationSamplingFactorLoggedin;
+ }
+
+ /**
+ * @override
+ * @inheritdoc
+ */
+ L.schema = 'MultimediaViewerDuration';
+
+ // eslint-disable-next-line valid-jsdoc
+ /**
+ * Saves the start of a duration
+ *
+ * @param {string|string[]} typeOrTypes Type(s) of duration being measured.
+ * @chainable
+ */
+ L.start = function ( typeOrTypes ) {
+ var i,
+ start = $.now();
+
+ if ( !typeOrTypes ) {
+ throw new Error( 'Must specify type' );
+ }
+
+ if ( !$.isArray( typeOrTypes ) ) {
+ typeOrTypes = [ typeOrTypes ];
+ }
+
+ for ( i = 0; i < typeOrTypes.length; i++ ) {
+ // Don't overwrite an existing value
+ if ( !this.starts.hasOwnProperty( typeOrTypes[ i ] ) ) {
+ this.starts[ typeOrTypes[ i ] ] = start;
+ }
+ }
+
+ return this;
+ };
+
+ // eslint-disable-next-line valid-jsdoc
+ /**
+ * Saves the stop of a duration
+ *
+ * @param {string} type Type of duration being measured.
+ * @param {number} start Start timestamp to substitute the one coming from start()
+ * @chainable
+ */
+ L.stop = function ( type, start ) {
+ var stop = $.now();
+
+ if ( !type ) {
+ throw new Error( 'Must specify type' );
+ }
+
+ // Don't overwrite an existing value
+ if ( !this.stops.hasOwnProperty( type ) ) {
+ this.stops[ type ] = stop;
+ }
+
+ // Don't overwrite an existing value
+ if ( start !== undefined && !this.starts.hasOwnProperty( type ) ) {
+ this.starts[ type ] = start;
+ }
+
+ return this;
+ };
+
+ // eslint-disable-next-line valid-jsdoc
+ /**
+ * Records the duration log event
+ *
+ * @param {string} type Type of duration being measured.
+ * @param {Object} extraData Extra information to add to the log event data
+ * @chainable
+ */
+ L.record = function ( type, extraData ) {
+ var e, duration;
+
+ if ( !type ) {
+ throw new Error( 'Must specify type' );
+ }
+
+ if ( !this.starts.hasOwnProperty( type ) || this.starts[ type ] === undefined ) {
+ return;
+ }
+
+ if ( !this.stops.hasOwnProperty( type ) || this.stops[ type ] === undefined ) {
+ return;
+ }
+
+ duration = this.stops[ type ] - this.starts[ type ];
+
+ e = {
+ type: type,
+ duration: duration,
+ loggedIn: !mw.user.isAnon(),
+ samplingFactor: this.samplingFactor
+ };
+
+ if ( extraData ) {
+ $.each( extraData, function ( key, value ) {
+ e[ key ] = value;
+ } );
+ }
+
+ if ( this.isEnabled() ) {
+ mw.log( 'mw.mmw.logger.DurationLogger', e );
+ }
+
+ this.log( e );
+
+ delete this.starts[ type ];
+ delete this.stops[ type ];
+
+ return this;
+ };
+
+ mw.mmv.durationLogger = new DurationLogger();
+}( mediaWiki, jQuery, OO ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/logging/mmv.logging.Logger.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/logging/mmv.logging.Logger.js
new file mode 100644
index 00000000..10bad51b
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/logging/mmv.logging.Logger.js
@@ -0,0 +1,160 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $ ) {
+ var L;
+
+ /**
+ * Abstract class providing common code for EventLogging loggers
+ *
+ * @class mw.mmv.logging.Logger
+ * @abstract
+ */
+ function Logger() {
+ this.Geo = undefined;
+ this.eventLog = undefined;
+ }
+
+ L = Logger.prototype;
+
+ /**
+ * Sampling factor key-value map.
+ *
+ * Makes the logger sample log events instead of recording each one if > 0. Disables logging if === 0.
+ * @property {number}
+ */
+ L.samplingFactor = 0;
+
+ /**
+ * EventLogging schema
+ * @property {string}
+ */
+ L.schema = '';
+
+ /**
+ * Sets the Geo object providing country information about the visitor
+ *
+ * @param {Object} Geo object containing country GeoIP information about the user
+ */
+ L.setGeo = function ( Geo ) {
+ this.Geo = Geo;
+ };
+
+ /**
+ * Sets the eventLog object providing a facility to record events
+ *
+ * @param {mw.eventLog} eventLog EventLogging instance
+ */
+ L.setEventLog = function ( eventLog ) {
+ this.eventLog = eventLog;
+ };
+
+ /**
+ * Loads the dependencies that allow us to log events
+ *
+ * @return {jQuery.Promise}
+ */
+ L.loadDependencies = function () {
+ var self = this,
+ waitForEventLog = $.Deferred();
+
+ // Waits for dom readiness because we don't want to have these dependencies loaded in the head
+ $( function () {
+ // window.Geo is currently defined in components that are loaded independently, there is no cheap
+ // way to load just that information. Either we piggy-back on something that already loaded it
+ // or we just don't have it
+ if ( window.Geo ) {
+ self.setGeo( window.Geo );
+ }
+
+ try {
+ mw.loader.using( [ 'ext.eventLogging', 'schema.' + self.schema ], function () {
+ self.setEventLog( mw.eventLog );
+ waitForEventLog.resolve();
+ } );
+ } catch ( e ) {
+ waitForEventLog.reject();
+ }
+ } );
+
+ return waitForEventLog;
+ };
+
+ /**
+ * Returns whether or not we should measure this request
+ *
+ * @return {boolean} True if this request needs to be sampled
+ */
+ L.isInSample = function () {
+ if ( !$.isNumeric( this.samplingFactor ) || this.samplingFactor < 1 ) {
+ return false;
+ }
+
+ return Math.floor( Math.random() * this.samplingFactor ) === 0;
+ };
+
+ /**
+ * Returns whether logging this event is enabled. This is intended for console logging, which
+ * (in debug mode) should be done even if the request is not being sampled, as long as logging
+ * is enabled for some sample.
+ *
+ * @return {boolean} True if this logging is enabled
+ */
+ L.isEnabled = function () {
+ return $.isNumeric( this.samplingFactor ) && this.samplingFactor >= 1;
+ };
+
+ /**
+ * True if the schema has a country field. Broken out in a separate function so it's easy to mock.
+ *
+ * @return {boolean}
+ */
+ L.schemaSupportsCountry = function () {
+ return this.eventLog && this.eventLog.schemas && // don't die if eventLog is a mock
+ this.schema in this.eventLog.schemas && // don't die if schema is not loaded
+ 'country' in this.eventLog.schemas[ this.schema ].schema.properties;
+ };
+
+ /**
+ * Logs EventLogging data while including Geo data if any
+ *
+ * @param {Object} data
+ * @return {jQuery.Promise}
+ */
+ L.log = function ( data ) {
+ var self = this;
+
+ if ( self.isInSample() ) {
+ return this.loadDependencies().then( function () {
+ // Add Geo information if there's any
+ if (
+ self.Geo && self.Geo.country !== undefined &&
+ self.schemaSupportsCountry()
+ ) {
+ data.country = self.Geo.country;
+ }
+
+ self.eventLog.logEvent( self.schema, data );
+ } );
+ } else {
+ return $.Deferred().resolve();
+ }
+ };
+
+ mw.mmv.logging = {};
+ mw.mmv.logging.Logger = Logger;
+}( mediaWiki, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/logging/mmv.logging.PerformanceLogger.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/logging/mmv.logging.PerformanceLogger.js
new file mode 100644
index 00000000..026626ff
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/logging/mmv.logging.PerformanceLogger.js
@@ -0,0 +1,455 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $, oo ) {
+ var PL;
+
+ /**
+ * Measures the network performance
+ * See <https://meta.wikimedia.org/wiki/Schema:MultimediaViewerNetworkPerformance>
+ *
+ * @class mw.mmv.logging.PerformanceLogger
+ * @extends mw.mmv.logging.Logger
+ * @constructor
+ */
+ function PerformanceLogger() {}
+
+ oo.inheritClass( PerformanceLogger, mw.mmv.logging.Logger );
+
+ PL = PerformanceLogger.prototype;
+
+ /**
+ * @override
+ * @inheritdoc
+ */
+ PL.samplingFactor = mw.config.get( 'wgMultimediaViewer' ).networkPerformanceSamplingFactor;
+
+ /**
+ * @override
+ * @inheritdoc
+ */
+ PL.schema = 'MultimediaViewerNetworkPerformance';
+
+ /**
+ * Global setup that should be done while the page loads
+ */
+ PL.init = function () {
+ var performance = this.getWindowPerformance();
+
+ // by default logging is cut off after 150 resources, which is not enough in debug mode
+ // only supported by IE
+ if ( mw.config.get( 'debug' ) && performance && performance.setResourceTimingBufferSize ) {
+ performance.setResourceTimingBufferSize( 500 );
+ }
+ };
+
+ /**
+ * Gather network performance for a given URL
+ * Will only run on a sample of users/requests. Avoid using this on URLs that aren't
+ * cached by the browser, as it will consume unnecessary bandwidth for the user.
+ *
+ * @param {string} type the type of request to be measured
+ * @param {string} url URL to be measured
+ * @param {jQuery.Deferred.<string>} [extraStatsDeferred] A promise which resolves to the extra stats.
+ * @return {jQuery.Promise} A promise that resolves when the contents of the URL have been fetched
+ */
+ PL.record = function ( type, url, extraStatsDeferred ) {
+ var deferred = $.Deferred(),
+ request,
+ perf = this,
+ start;
+
+ try {
+ request = this.newXHR();
+
+ request.onprogress = function ( e ) {
+ var percent;
+
+ if ( e.lengthComputable ) {
+ percent = ( e.loaded / e.total ) * 100;
+ }
+
+ deferred.notify( request.response, percent );
+ };
+
+ request.onreadystatechange = function () {
+ var total = $.now() - start;
+
+ if ( request.readyState === 4 ) {
+ deferred.notify( request.response, 100 );
+ deferred.resolve( request.response );
+ perf.recordEntryDelayed( type, total, url, request, extraStatsDeferred );
+ }
+ };
+
+ start = $.now();
+ request.open( 'GET', url, true );
+ request.send();
+ } catch ( e ) {
+ // old browser not supporting XMLHttpRequest or CORS, or CORS is not permitted
+ return deferred.reject();
+ }
+
+ return deferred;
+ };
+
+ /**
+ * Records network performance results for a given url
+ * Will record if enough data is present and it's not a local cache hit
+ *
+ * @param {string} type the type of request to be measured
+ * @param {number} total the total load time tracked with a basic technique
+ * @param {string} url URL of that was measured
+ * @param {XMLHttpRequest} request HTTP request that just completed
+ * @param {jQuery.Deferred.<string>} [extraStatsDeferred] A promise which resolves to extra stats to be included.
+ * @return {jQuery.Promise}
+ */
+ PL.recordEntry = function ( type, total, url, request, extraStatsDeferred ) {
+ var matches,
+ logger = this,
+ stats = { type: type,
+ contentHost: window.location.host,
+ isHttps: window.location.protocol === 'https:',
+ total: total },
+ connection = this.getNavigatorConnection();
+
+ if ( !this.performanceChecked ) {
+ this.performanceChecked = {};
+ }
+
+ if ( url && url.length ) {
+ // There is no need to measure the same url more than once
+ if ( url in this.performanceChecked ) {
+ return $.Deferred().reject();
+ }
+
+ this.performanceChecked[ url ] = true;
+
+ matches = url.match( /^https?:\/\/([^/?#]+)(?:[/?#]|$)/i );
+ stats.isHttps = url.indexOf( 'https' ) === 0;
+ }
+
+ if ( !matches || matches.length !== 2 ) {
+ stats.urlHost = stats.contentHost;
+ } else {
+ stats.urlHost = matches[ 1 ];
+ }
+
+ this.populateStatsFromXhr( stats, request );
+ this.populateStatsFromPerformance( stats, url );
+
+ // Add connection information if there's any
+ if ( connection ) {
+ if ( connection.bandwidth ) {
+ if ( connection.bandwidth === Infinity ) {
+ stats.bandwidth = -1;
+ } else {
+ stats.bandwidth = Math.round( connection.bandwidth );
+ }
+ }
+
+ if ( connection.metered ) {
+ stats.metered = connection.metered;
+ }
+ }
+
+ return ( extraStatsDeferred || $.Deferred().reject() ).done( function ( extraStats ) {
+ stats = $.extend( stats, extraStats );
+ } ).always( function () {
+ logger.log( stats );
+ } );
+ };
+
+ /**
+ * Processes an XMLHttpRequest (or jqXHR) object
+ *
+ * @param {Object} stats stats object to extend with additional statistics fields
+ * @param {XMLHttpRequest} request
+ */
+ PL.populateStatsFromXhr = function ( stats, request ) {
+ var age,
+ contentLength,
+ xcache,
+ xvarnish,
+ varnishXCache,
+ lastModified;
+
+ if ( !request ) {
+ return;
+ }
+
+ stats.status = request.status;
+
+ // Chrome disallows header access for CORS image requests, even if the responose has the
+ // proper header :-/
+ contentLength = request.getResponseHeader( 'Content-Length' );
+ if ( contentLength === null ) {
+ return;
+ }
+
+ xcache = request.getResponseHeader( 'X-Cache' );
+ if ( xcache ) {
+ stats.XCache = xcache;
+ varnishXCache = this.parseVarnishXCacheHeader( xcache );
+
+ $.each( varnishXCache, function ( key, value ) {
+ stats[ key ] = value;
+ } );
+ }
+
+ xvarnish = request.getResponseHeader( 'X-Varnish' );
+ if ( xvarnish ) {
+ stats.XVarnish = xvarnish;
+ }
+
+ stats.contentLength = parseInt( contentLength, 10 );
+
+ age = parseInt( request.getResponseHeader( 'Age' ), 10 );
+ if ( !isNaN( age ) ) {
+ stats.age = age;
+ }
+
+ stats.timestamp = new Date( request.getResponseHeader( 'Date' ) ).getTime() / 1000;
+
+ lastModified = request.getResponseHeader( 'Last-Modified' );
+ if ( lastModified ) {
+ stats.lastModified = new Date( lastModified ).getTime() / 1000;
+ }
+ };
+
+ /**
+ * Populates statistics based on the Request Timing API
+ *
+ * @param {Object} stats
+ * @param {string} url
+ */
+ PL.populateStatsFromPerformance = function ( stats, url ) {
+ var performance = this.getWindowPerformance(),
+ timingEntries, timingEntry;
+
+ // If we're given an xhr and we have access to the Navigation Timing API, use it
+ if ( performance && performance.getEntriesByName ) {
+ // This could be tricky as we need to match encoding (the Request Timing API uses
+ // percent-encoded UTF-8). The main use case we are interested in is thumbnails and
+ // jQuery AJAX. jQuery uses encodeURIComponent to construct URL parameters, and
+ // thumbnail URLs come from MediaWiki API which also encodes them, so both should be
+ // all right.
+ timingEntries = performance.getEntriesByName( url );
+
+ if ( timingEntries.length ) {
+ // Let's hope it's the first request for the given URL we are interested in.
+ // This could fail in exotic cases (e.g. we send an AJAX request for a thumbnail,
+ // but it exists on the page as a normal thumbnail with the exact same size),
+ // but it's unlikely.
+ timingEntry = timingEntries[ 0 ];
+
+ stats.total = Math.round( timingEntry.duration );
+ stats.redirect = Math.round( timingEntry.redirectEnd - timingEntry.redirectStart );
+ stats.dns = Math.round( timingEntry.domainLookupEnd - timingEntry.domainLookupStart );
+ stats.tcp = Math.round( timingEntry.connectEnd - timingEntry.connectStart );
+ stats.request = Math.round( timingEntry.responseStart - timingEntry.requestStart );
+ stats.response = Math.round( timingEntry.responseEnd - timingEntry.responseStart );
+ stats.cache = Math.round( timingEntry.domainLookupStart - timingEntry.fetchStart );
+ } else if ( performance.getEntriesByType( 'resource' ).length === 150 && this.isEnabled() ) {
+ // browser stops logging after 150 entries
+ mw.log( 'performance buffer full, results are probably incorrect' );
+ }
+ }
+ };
+
+ /**
+ * Like recordEntry, but takes a jqXHR argument instead of a normal XHR one.
+ * Due to the way some parameters are retrieved, this will work best if the context option
+ * for the ajax request was not used.
+ *
+ * @param {string} type the type of request to be measured
+ * @param {number} total the total load time tracked with a basic technique
+ * @param {jqXHR} jqxhr
+ */
+ PL.recordJQueryEntry = function ( type, total, jqxhr ) {
+ var perf = this;
+
+ // We take advantage of the fact that the context of the jqXHR deferred is the AJAX
+ // settings object. The deferred has already resolved so chaining to it does not influence
+ // the timing.
+ jqxhr.done( function () {
+ var url;
+
+ if ( !this.url ) {
+ mw.log.warn( 'Cannot find URL - did you use context option?' );
+ } else {
+ url = this.url;
+ // The performance API returns absolute URLs, but the one in the settings object is
+ // usually relative.
+ if ( !url.match( /^(\w+:)?\/\// ) ) {
+ url = location.protocol + '//' + location.host + url;
+ }
+ }
+
+ if ( this.crossDomain && this.dataType === 'jsonp' ) {
+ // Cross-domain jQuery requests return a fake jqXHR object which is useless and
+ // would only cause logging errors.
+ jqxhr = undefined;
+ }
+
+ // jQuery does not expose the original XHR object, but the jqXHR wrapper is similar
+ // enogh that we will probably get away by passing it instead.
+ perf.recordEntry( type, total, url, jqxhr );
+ } );
+ };
+
+ /**
+ * Records network performance results for a given url
+ * Will record if enough data is present and it's not a local cache hit
+ * Will run after a delay to make sure the window.performance entry is present
+ *
+ * @param {string} type the type of request to be measured
+ * @param {number} total the total load time tracked with a basic technique
+ * @param {string} url URL of that was measured
+ * @param {XMLHttpRequest} request HTTP request that just completed
+ * @param {jQuery.Promise.<string>} extraStatsDeferred A promise which resolves to extra stats.
+ */
+ PL.recordEntryDelayed = function ( type, total, url, request, extraStatsDeferred ) {
+ var perf = this;
+
+ // The timeout is necessary because if there's an entry in window.performance,
+ // it hasn't been added yet at this point
+ setTimeout( function () {
+ perf.recordEntry( type, total, url, request, extraStatsDeferred );
+ }, 0 );
+ };
+
+ /**
+ * Like recordEntryDelayed, but for jQuery AJAX requests.
+ *
+ * @param {string} type the type of request to be measured
+ * @param {number} total the total load time tracked with a basic technique
+ * @param {jqXHR} jqxhr
+ */
+ PL.recordJQueryEntryDelayed = function ( type, total, jqxhr ) {
+ var perf = this;
+
+ // The timeout is necessary because if there's an entry in window.performance,
+ // it hasn't been added yet at this point
+ setTimeout( function () {
+ perf.recordJQueryEntry( type, total, jqxhr );
+ }, 0 );
+ };
+
+ /**
+ * Parses an X-Cache header from Varnish and extracts varnish information
+ *
+ * @param {string} header The X-Cache header from the request
+ * @return {Object} The parsed X-Cache data
+ */
+ PL.parseVarnishXCacheHeader = function ( header ) {
+ var parts,
+ part,
+ subparts,
+ i,
+ results = {},
+ matches;
+
+ if ( !header || !header.length ) {
+ return results;
+ }
+
+ parts = header.split( ',' );
+
+ for ( i = 0; i < parts.length; i++ ) {
+ part = parts[ i ];
+ subparts = part.trim().split( ' ' );
+
+ // If the subparts aren't space-separated, it's an unknown format, skip
+ if ( subparts.length < 2 ) {
+ continue;
+ }
+
+ matches = part.match( /\(([0-9]+)\)/ );
+
+ // If there is no number between parenthesis for a given server
+ // it's an unknown format, skip
+ if ( !matches || matches.length !== 2 ) {
+ continue;
+ }
+
+ results[ 'varnish' + ( i + 1 ) ] = subparts[ 0 ];
+ results[ 'varnish' + ( i + 1 ) + 'hits' ] = parseInt( matches[ 1 ], 10 );
+ }
+
+ return results;
+ };
+
+ /**
+ * Returns the window's Performance object
+ * Allows us to override for unit tests
+ *
+ * @return {Object} The window's Performance object
+ */
+ PL.getWindowPerformance = function () {
+ return window.performance;
+ };
+
+ /**
+ * Returns the navigator's Connection object
+ * Allows us to override for unit tests
+ *
+ * @return {Object} The navigator's Connection object
+ */
+ PL.getNavigatorConnection = function () {
+ return navigator.connection || navigator.mozConnection || navigator.webkitConnection;
+ };
+
+ /**
+ * Returns a new XMLHttpRequest object
+ * Allows us to override for unit tests
+ *
+ * @return {XMLHttpRequest} New XMLHttpRequest
+ */
+ PL.newXHR = function () {
+ return new XMLHttpRequest();
+ };
+
+ /**
+ * @override
+ * @inheritdoc
+ */
+ PL.log = function ( data ) {
+ var trackedWidths = mw.mmv.ThumbnailWidthCalculator.prototype.defaultOptions.widthBuckets.slice( 0 );
+ trackedWidths.push( 600 ); // Most common non-bucket size
+
+ // Track thumbnail load time with statsv, sampled
+ if ( this.isInSample() &&
+ data.type === 'image' &&
+ data.imageWidth > 0 &&
+ data.total > 20 &&
+ $.inArray( data.imageWidth, trackedWidths ) !== -1
+ ) {
+ mw.track( 'timing.media.thumbnail.client.' + data.imageWidth, data.total );
+ }
+
+ if ( this.isEnabled() ) {
+ mw.log( 'mw.mmv.logging.PerformanceLogger', data );
+ }
+ return mw.mmv.logging.Logger.prototype.log.call( this, data );
+ };
+
+ new PerformanceLogger().init();
+
+ mw.mmv.logging.PerformanceLogger = PerformanceLogger;
+
+}( mediaWiki, jQuery, OO ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/logging/mmv.logging.ViewLogger.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/logging/mmv.logging.ViewLogger.js
new file mode 100644
index 00000000..34dcc9a0
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/logging/mmv.logging.ViewLogger.js
@@ -0,0 +1,178 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $ ) {
+ var VL;
+
+ /**
+ * Tracks how long users are viewing images for
+ *
+ * @class mw.mmv.logging.ViewLogger
+ * @extends mw.Api
+ * @constructor
+ * @param {mw.mmv.Config} config mw.mmv.Config object
+ * @param {Object} windowObject Browser window object
+ * @param {mw.mmv.logging.ActionLogger} actionLogger ActionLogger object
+ */
+ function ViewLogger( config, windowObject, actionLogger ) {
+ /**
+ * Was the last image view logged or was logging skipped?
+ * @property {boolean}
+ */
+ this.wasLastViewLogged = false;
+
+ /**
+ * Record when the user started looking at the current image
+ * @property {number}
+ */
+ this.viewStartTime = 0;
+
+ /**
+ * How long the user has been looking at the current image
+ * @property {number}
+ */
+ this.viewDuration = 0;
+
+ /**
+ * The image URL to record a virtual view for
+ * @property {string}
+ */
+ this.url = '';
+
+ /**
+ * If set, URI to send the beacon request to in order to record the virtual view
+ * @property {string}
+ */
+ this.recordVirtualViewBeaconURI = config.recordVirtualViewBeaconURI();
+
+ /**
+ * Browser window
+ * @property {Object}
+ */
+ this.window = windowObject;
+
+ /**
+ * Action logger
+ * @property {mw.mmv.logging.ActionLogger}
+ */
+ this.actionLogger = actionLogger;
+ }
+
+ VL = ViewLogger.prototype;
+
+ /**
+ * Tracks the unview event of the current image if appropriate
+ */
+ VL.unview = function () {
+ if ( !this.wasLastViewLogged ) {
+ return;
+ }
+
+ this.wasLastViewLogged = false;
+ this.actionLogger.log( 'image-unview', true );
+ };
+
+ /**
+ * Starts recording a viewing window for the current image
+ */
+ VL.startViewDuration = function () {
+ this.viewStartTime = $.now();
+ };
+
+ /**
+ * Stops recording the viewing window for the current image
+ */
+ VL.stopViewDuration = function () {
+ if ( this.viewStartTime ) {
+ this.viewDuration += $.now() - this.viewStartTime;
+ this.viewStartTime = 0;
+ }
+ };
+
+ /**
+ * Records the amount of time the current image has been viewed
+ */
+ VL.recordViewDuration = function () {
+ var uri;
+
+ this.stopViewDuration();
+
+ if ( this.recordVirtualViewBeaconURI ) {
+ uri = new mw.Uri( this.recordVirtualViewBeaconURI );
+ uri.extend( { duration: this.viewDuration,
+ uri: this.url } );
+
+ try {
+ navigator.sendBeacon( uri.toString() );
+ } catch ( e ) {
+ $.ajax( {
+ type: 'HEAD',
+ url: uri.toString()
+ } );
+ }
+
+ mw.log( 'Image has been viewed for ', this.viewDuration );
+ }
+
+ this.viewDuration = 0;
+
+ this.unview();
+ };
+
+ /**
+ * Sets up the view tracking for the current image
+ *
+ * @param {string} url URL of the image to record a virtual view for
+ */
+ VL.attach = function ( url ) {
+ var view = this;
+
+ this.url = url;
+ this.startViewDuration();
+
+ $( this.window )
+ .off( '.mmv-view-logger' )
+ .on( 'beforeunload.mmv-view-logger', function () {
+ view.recordViewDuration();
+ } )
+ .on( 'focus.mmv-view-logger', function () {
+ view.startViewDuration();
+ } )
+ .on( 'blur.mmv-view-logger', function () {
+ view.stopViewDuration();
+ } );
+ };
+
+ /*
+ * Stops listening to events
+ */
+ VL.unattach = function () {
+ $( this.window ).off( '.mmv-view-logger' );
+ this.stopViewDuration();
+ };
+
+ /**
+ * Tracks whether or not the image view event was logged or not (i.e. was it in the logging sample)
+ *
+ * @param {boolean} wasEventLogged Whether the image view event was logged
+ */
+ VL.setLastViewLogged = function ( wasEventLogged ) {
+ this.wasLastViewLogged = wasEventLogged;
+ };
+
+ mw.mmv.logging.ViewLogger = ViewLogger;
+}( mediaWiki, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.Config.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.Config.js
new file mode 100644
index 00000000..a2989816
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.Config.js
@@ -0,0 +1,272 @@
+/*
+ * This file is part of the MediaWiki extension MediaViewer.
+ *
+ * MediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $ ) {
+ var CP;
+
+ /**
+ * Contains/retrieves configuration/environment information for MediaViewer.
+ * @class mw.mmv.Config
+ * @constructor
+ * @param {Object} viewerConfig
+ * @param {mw.Map} mwConfig
+ * @param {Object} mwUser
+ * @param {mw.Api} api
+ * @param {mw.storage} localStorage
+ */
+ function Config( viewerConfig, mwConfig, mwUser, api, localStorage ) {
+ /**
+ * A plain object storing MediaViewer-specific settings
+ * @type {Object}
+ */
+ this.viewerConfig = viewerConfig;
+
+ /**
+ * The mw.config object, for dependency injection
+ * @type {mw.Map}
+ */
+ this.mwConfig = mwConfig;
+
+ /**
+ * mw.user object, for dependency injection
+ * @type {Object}
+ */
+ this.mwUser = mwUser;
+
+ /**
+ * API object, for dependency injction
+ * @type {mw.Api}
+ */
+ this.api = api;
+
+ /**
+ * The localStorage object, for dependency injection
+ * @type {mw.storage}
+ */
+ this.localStorage = localStorage;
+ }
+ CP = Config.prototype;
+
+ /**
+ * Get value from local storage or fail gracefully.
+ *
+ * @param {string} key
+ * @param {*} [fallback] value to return when key is not set or localStorage is not supported
+ * @return {string|null} stored value or fallback or null if neither exists
+ */
+ CP.getFromLocalStorage = function ( key, fallback ) {
+ var value = this.localStorage.get( key );
+
+ // localStorage will only store strings; if values `null`, `false` or
+ // `0` are set, they'll come out as `"null"`, `"false"` or `"0"`, so we
+ // can be certain that an actual null is a failure to locate the item,
+ // and false is an issue with localStorage itself
+ if ( value !== null && value !== false ) {
+ return value;
+ }
+
+ if ( value === null ) {
+ mw.log( 'Failed to fetch item ' + key + ' from localStorage' );
+ }
+
+ return fallback !== undefined ? fallback : null;
+ };
+
+ /**
+ * Set item in local storage or fail gracefully.
+ *
+ * @param {string} key
+ * @param {*} value
+ * @return {boolean} whether storing the item was successful
+ */
+ CP.setInLocalStorage = function ( key, value ) {
+ return this.localStorage.set( key, value );
+ };
+
+ /**
+ * Remove item from local storage or fail gracefully.
+ *
+ * @param {string} key
+ * @return {boolean} whether storing the item was successful
+ */
+ CP.removeFromLocalStorage = function ( key ) {
+ this.localStorage.remove( key );
+
+ // mw.storage.remove catches all exceptions and returns false if any
+ // occur, so we can't distinguish between actual issues, and
+ // localStorage not being supported - however, localStorage.removeItem
+ // is not documented to throw any errors, so nothing to worry about;
+ // when localStorage is not supported, we'll consider removal successful
+ // (it can't have been there in the first place)
+ return true;
+ };
+
+ /**
+ * Set user preference via AJAX
+ *
+ * @param {string} key
+ * @param {string} value
+ * @return {jQuery.Promise} a deferred which resolves/rejects on success/failure respectively
+ */
+ CP.setUserPreference = function ( key, value ) {
+ return this.api.saveOption( key, value );
+ };
+
+ /**
+ * Returns true if MediaViewer should handle thumbnail clicks.
+ *
+ * @return {boolean}
+ */
+ CP.isMediaViewerEnabledOnClick = function () {
+ // IMPORTANT: mmv.head.js uses the same logic but does not use this class to be lightweight. Make sure to keep it in sync.
+ return this.mwConfig.get( 'wgMediaViewer' ) && // global opt-out switch, can be set in user JS
+ this.mwConfig.get( 'wgMediaViewerOnClick' ) && // thumbnail opt-out, can be set in preferences
+ ( !this.mwUser.isAnon() || this.getFromLocalStorage( 'wgMediaViewerOnClick', '1' ) === '1' ); // thumbnail opt-out for anons
+ };
+
+ /**
+ * (Semi-)permanently stores the setting whether MediaViewer should handle thumbnail clicks.
+ * - for logged-in users, we use preferences
+ * - for anons, we use localStorage
+ * - for anons with old browsers, we don't do anything
+ *
+ * @param {boolean} enabled
+ * @return {jQuery.Promise} a deferred which resolves/rejects on success/failure respectively
+ */
+ CP.setMediaViewerEnabledOnClick = function ( enabled ) {
+ var deferred,
+ newPrefValue,
+ defaultPrefValue = this.mwConfig.get( 'wgMediaViewerEnabledByDefault' ),
+ config = this,
+ success = true;
+
+ if ( this.mwUser.isAnon() ) {
+ if ( !enabled ) {
+ success = this.setInLocalStorage( 'wgMediaViewerOnClick', '0' ); // localStorage stringifies everything, best use strings in the first place
+ } else {
+ success = this.removeFromLocalStorage( 'wgMediaViewerOnClick' );
+ }
+ if ( success ) {
+ deferred = $.Deferred().resolve();
+ } else {
+ deferred = $.Deferred().reject();
+ }
+ } else {
+ // Simulate changing the option in Special:Preferences. Turns out this is quite hard (bug 69942):
+ // we need to delete the user_properties row if the new setting is the same as the default,
+ // otherwise set '1' for enabled, '' for disabled. In theory the pref API will delete the row
+ // if the new value equals the default, but this does not always work.
+ if ( defaultPrefValue === true ) {
+ newPrefValue = enabled ? '1' : '';
+ } else {
+ // undefined will cause the API call to omit the optionvalue parameter
+ // which in turn will cause the options API to delete the row and revert the pref to default
+ newPrefValue = enabled ? '1' : undefined;
+ }
+ deferred = this.setUserPreference( 'multimediaviewer-enable', newPrefValue );
+ }
+
+ return deferred.done( function () {
+ // make the change work without a reload
+ config.mwConfig.set( 'wgMediaViewerOnClick', enabled );
+ if ( !enabled ) {
+ // set flag for showing a popup if this was a first-time disable
+ config.maybeEnableStatusInfo();
+ }
+ } );
+ };
+
+ /**
+ * True if info about enable/disable status should be displayed (mingle #719).
+ *
+ * @return {boolean}
+ */
+ CP.shouldShowStatusInfo = function () {
+ return !this.isMediaViewerEnabledOnClick() && this.getFromLocalStorage( 'mmv-showStatusInfo' ) === '1';
+ };
+
+ /**
+ * Called when MediaViewer is disabled. If status info was never displayed before, future
+ * shouldShowStatusInfo() calls will return true.
+ *
+ * @private
+ */
+ CP.maybeEnableStatusInfo = function () {
+ var currentShowStatusInfo = this.getFromLocalStorage( 'mmv-showStatusInfo' );
+ if ( currentShowStatusInfo === null ) {
+ this.setInLocalStorage( 'mmv-showStatusInfo', '1' );
+ }
+ };
+
+ /**
+ * Called when status info is displayed. Future shouldShowStatusInfo() calls will retrurn false.
+ */
+ CP.disableStatusInfo = function () {
+ this.setInLocalStorage( 'mmv-showStatusInfo', '0' );
+ };
+
+ /**
+ * Returns file extensions handled by Media Viewer.
+ *
+ * The object's keys are the file extensions.
+ * The object's values are either 'default' when Media Viewer handles that file extension
+ * directly or the name of a ResourceLoader module to load when such a file is opened.
+ *
+ * @return {Object}
+ */
+ CP.extensions = function () {
+ return this.viewerConfig.extensions;
+ };
+
+ /**
+ * Returns UI language
+ *
+ * @return {string} Language code
+ */
+ CP.language = function () {
+ return this.mwConfig.get( 'wgUserLanguage', false ) || this.mwConfig.get( 'wgContentLanguage', 'en' );
+ };
+
+ /**
+ * Returns URI of virtual view beacon or false if not set
+ *
+ * @return {string|boolean} URI
+ */
+ CP.recordVirtualViewBeaconURI = function () {
+ return this.viewerConfig.recordVirtualViewBeaconURI;
+ };
+
+ /**
+ * Returns useThumbnailGuessing flag
+ *
+ * @return {boolean}
+ */
+ CP.useThumbnailGuessing = function () {
+ return this.viewerConfig.useThumbnailGuessing;
+ };
+
+ /**
+ * Returns imageQueryParameter, if set
+ *
+ * @return {string|boolean}
+ */
+ CP.imageQueryParameter = function () {
+ return this.viewerConfig.imageQueryParameter;
+ };
+
+ mw.mmv.Config = Config;
+}( mediaWiki, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.EmbedFileFormatter.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.EmbedFileFormatter.js
new file mode 100644
index 00000000..735173bc
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.EmbedFileFormatter.js
@@ -0,0 +1,251 @@
+/*
+ * This file is part of the MediaWiki extension MediaViewer.
+ *
+ * MediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $ ) {
+ var EFFP;
+
+ /**
+ * Converts data in various formats needed by the Embed sub-dialog
+ *
+ * @class mw.mmv.EmbedFileFormatter
+ * @constructor
+ */
+ function EmbedFileFormatter() {
+ /** @property {mw.mmv.HtmlUtils} htmlUtils - */
+ this.htmlUtils = new mw.mmv.HtmlUtils();
+
+ /**
+ * @property {mw.mmv.routing.Router} router -
+ */
+ this.router = new mw.mmv.routing.Router();
+ }
+ EFFP = EmbedFileFormatter.prototype;
+
+ /**
+ * Returns the caption of the image (possibly a fallback generated from image metadata).
+ *
+ * @param {mw.mmv.model.EmbedFileInfo} info
+ * @return {string}
+ */
+ EFFP.getCaption = function ( info ) {
+ if ( info.caption ) {
+ return this.htmlUtils.htmlToText( info.caption );
+ } else {
+ return info.imageInfo.title.getNameText();
+ }
+ };
+
+ /**
+ * Helper function to generate thumbnail wikicode
+ *
+ * @param {mw.Title} title
+ * @param {number} [width]
+ * @param {string} [caption]
+ * @param {string} [alt]
+ * @return {string}
+ */
+ EFFP.getThumbnailWikitext = function ( title, width, caption, alt ) {
+ var widthSection, captionSection, altSection;
+
+ widthSection = width ? '|' + width + 'px' : '';
+ captionSection = caption ? '|' + caption : '';
+ altSection = alt ? '|alt=' + alt : '';
+
+ return '[[File:' + title.getMainText() + widthSection + '|thumb' + captionSection + altSection + ']]';
+ };
+
+ /**
+ * Helper function to generate thumbnail wikicode
+ *
+ * @param {mw.mmv.model.EmbedFileInfo} info
+ * @param {number} [width]
+ * @return {string}
+ */
+ EFFP.getThumbnailWikitextFromEmbedFileInfo = function ( info, width ) {
+ return this.getThumbnailWikitext( info.imageInfo.title, width, this.getCaption( info ), info.alt );
+ };
+
+ /**
+ * Byline construction
+ *
+ * @param {string} [author] author name (can contain HTML)
+ * @param {string} [source] source name (can contain HTML)
+ * @param {string} [attribution] custom attribution line (can contain HTML)
+ * @param {Function} [formatterFunction] Format function for the text - defaults to whitelisting HTML links, but all else sanitized.
+ * @return {string} Byline (can contain HTML)
+ */
+ EFFP.getByline = function ( author, source, attribution, formatterFunction ) {
+ var formatter = this;
+
+ formatterFunction = formatterFunction || function ( txt ) {
+ return formatter.htmlUtils.htmlToTextWithLinks( txt );
+ };
+
+ if ( attribution ) {
+ attribution = attribution && formatterFunction( attribution );
+ return attribution;
+ } else {
+ author = author && formatterFunction( author );
+ source = source && formatterFunction( source );
+
+ if ( author && source ) {
+ return mw.message(
+ 'multimediaviewer-credit',
+ author,
+ source
+ ).parse();
+ } else {
+ return author || source;
+ }
+ }
+ };
+
+ /**
+ * Generates the plain text embed code for the image credit line.
+ *
+ * @param {mw.mmv.model.EmbedFileInfo} info
+ * @return {string}
+ */
+ EFFP.getCreditText = function ( info ) {
+ var creditText, creditParams,
+ formatter = this,
+ shortURL = info.imageInfo.descriptionShortUrl,
+ license = info.imageInfo.license,
+ byline = this.getByline( info.imageInfo.author, info.imageInfo.source, info.imageInfo.attribution, function ( txt ) {
+ return formatter.htmlUtils.htmlToText( txt );
+ } );
+
+ // If both the byline and licence are missing, the credit text is simply the URL
+ if ( !byline && !license ) {
+ return shortURL;
+ }
+
+ creditParams = [
+ 'multimediaviewer-text-embed-credit-text-'
+ ];
+
+ if ( byline ) {
+ creditParams[ 0 ] += 'b';
+ creditParams.push( byline );
+ }
+
+ if ( license ) {
+ creditParams[ 0 ] += 'l';
+ creditParams.push( this.htmlUtils.htmlToText( license.getShortName() ) );
+ }
+
+ creditParams.push( shortURL );
+ creditText = mw.message.apply( mw, creditParams ).plain();
+
+ return creditText;
+ };
+
+ /**
+ * Generates the HTML embed code for the image credit line.
+ *
+ * @param {mw.mmv.model.EmbedFileInfo} info
+ * @return {string}
+ */
+ EFFP.getCreditHtml = function ( info ) {
+ var creditText, creditParams,
+ shortURL = info.imageInfo.descriptionShortUrl,
+ shortLink = this.htmlUtils.makeLinkText( mw.message( 'multimediaviewer-html-embed-credit-link-text' ), { href: shortURL } ),
+ license = info.imageInfo.license,
+ byline = this.getByline( info.imageInfo.author, info.imageInfo.source, info.imageInfo.attribution );
+
+ if ( !byline && !license ) {
+ return shortLink;
+ }
+
+ creditParams = [
+ 'multimediaviewer-html-embed-credit-text-'
+ ];
+
+ if ( byline ) {
+ creditParams[ 0 ] += 'b';
+ creditParams.push( byline );
+ }
+ if ( license ) {
+ creditParams[ 0 ] += 'l';
+ creditParams.push( license.getShortLink() );
+ }
+
+ creditParams.push( shortLink );
+ creditText = mw.message.apply( mw, creditParams ).plain();
+
+ return creditText;
+ };
+
+ /**
+ * Returns HTML code for a link to the site of the image.
+ *
+ * @param {mw.mmv.model.EmbedFileInfo} info
+ * @return {string}
+ */
+ EFFP.getSiteLink = function ( info ) {
+ var siteName = info.repoInfo.displayName,
+ siteUrl = info.repoInfo.getSiteLink();
+
+ if ( siteUrl ) {
+ return this.htmlUtils.jqueryToHtml(
+ $( '<a>' ).prop( 'href', siteUrl ).text( siteName )
+ );
+ } else {
+ return siteName;
+ }
+ };
+
+ /**
+ * Generates the HTML embed code for the image.
+ *
+ * @param {mw.mmv.model.EmbedFileInfo} info
+ * @param {string} imgUrl URL to the file itself.
+ * @param {number} [width] Width to put into the image element.
+ * @param {number} [height] Height to put into the image element.
+ * @return {string} Embed code.
+ */
+ EFFP.getThumbnailHtml = function ( info, imgUrl, width, height ) {
+ return this.htmlUtils.jqueryToHtml(
+ $( '<p>' ).append(
+ $( '<a>' )
+ .attr( 'href', this.getLinkUrl( info ) )
+ .append(
+ $( '<img>' )
+ .attr( 'src', imgUrl )
+ .attr( 'alt', info.alt || info.imageInfo.title.getMainText() )
+ .attr( 'height', height )
+ .attr( 'width', width )
+ ),
+ $( '<br>' ),
+ this.getCreditHtml( info )
+ )
+ );
+ };
+
+ /**
+ * Generate a link which we will be using for sharing stuff.
+ *
+ * @param {mw.mmv.model.EmbedFileInfo} info
+ * @return {string} URL
+ */
+ EFFP.getLinkUrl = function ( info ) {
+ var route = new mw.mmv.routing.ThumbnailRoute( info.imageInfo.title );
+ return this.router.createHashedUrl( route, info.imageInfo.descriptionUrl );
+ };
+
+ mw.mmv.EmbedFileFormatter = EmbedFileFormatter;
+}( mediaWiki, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.HtmlUtils.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.HtmlUtils.js
new file mode 100644
index 00000000..25cc65e7
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.HtmlUtils.js
@@ -0,0 +1,269 @@
+/*
+ * This file is part of the MediaWiki extension MediaViewer.
+ *
+ * MediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $ ) {
+ var HUP, cache;
+
+ /**
+ * Shared cache between HtmlUtils instances to store the results of expensive text operations.
+ *
+ * @member mw.mmv.HtmlUtils
+ * @private
+ * @static
+ * @type {{text: Object.<string, string>, textWithLinks: Object.<string, string>, textWithTags: Object.<string, string>}}
+ */
+ cache = {
+ text: {},
+ textWithLinks: {},
+ textWithTags: {}
+ };
+
+ /**
+ * Helper class that does various HTML-to-text transformations
+ *
+ * @class mw.mmv.HtmlUtils
+ * @constructor
+ */
+ function HtmlUtils() {}
+ HUP = HtmlUtils.prototype;
+
+ /**
+ * Returns a jQuery node which contains the given HTML (wrapped into a `<div>` - this is
+ * necessary since an arbitrary HTML string might not have a jQuery representation).
+ *
+ * @param {string|HTMLElement|jQuery} html
+ * @return {jQuery}
+ */
+ HUP.wrapAndJquerify = function ( html ) {
+ if ( this.isJQueryOrHTMLElement( html ) ) {
+ return $( '<div>' ).append( $( html ).clone() );
+ } else if ( typeof html === 'string' ) {
+ return $( '<div>' + html + '</div>' );
+ } else {
+ mw.log.warn( 'wrapAndJquerify: unknown type', html );
+ throw new Error( 'wrapAndJquerify: unknown type' );
+ }
+ };
+
+ /**
+ * Returns true of the object is a jQuery object or an HTMLElement, false otherwise
+ *
+ * @param {string|HTMLElement|jQuery} html
+ * @return {boolean}
+ */
+ HUP.isJQueryOrHTMLElement = function ( html ) {
+ if ( html instanceof jQuery ) {
+ return true;
+ }
+
+ if ( window.HTMLElement ) {
+ if ( html instanceof HTMLElement ) {
+ return true;
+ }
+ }
+
+ return false;
+ };
+
+ /**
+ * Filters display:none children of a node.
+ * The root element is never filtered, and generally ignored (i.e. whether the root element is
+ * visible won't affect the filtering).
+ * Works in place.
+ *
+ * @param {jQuery} $jq
+ */
+ HUP.filterInvisible = function ( $jq ) {
+ // We are not using :visible because
+ // 1) it would require appending $jq to the document which makes things complicated;
+ // 2) the main difference is that it looks for CSS rules hiding the element;
+ // since this function is intended to be used on html originating from a different
+ // document, possibly a different site, that would probably have unexpected results.
+ $jq
+ .find( '[style]' )
+ .filter( function () { return this.style.display === 'none'; } )
+ .remove();
+ };
+
+ /**
+ * Discards all nodes which do not match the whitelist,
+ * but keeps the text and whitelisted nodes inside them.
+ * Works in-place.
+ *
+ * @param {jQuery} $el
+ * @param {string} whitelist a jQuery selector string such as 'a, span, br'
+ */
+ HUP.whitelistHtml = function ( $el, whitelist ) {
+ var child, $prev,
+ $child = $el.children().first();
+
+ while ( $child && $child.length ) {
+ child = $child.get( 0 );
+
+ if ( child.nodeType !== child.ELEMENT_NODE ) {
+ return;
+ }
+
+ this.whitelistHtml( $child, whitelist );
+
+ if ( !$child.is( whitelist ) ) {
+ $prev = $child.prev();
+ $child.replaceWith( $child.contents() );
+ } else {
+ $prev = $child;
+ }
+
+ if ( $prev && $prev.length === 1 ) {
+ $child = $prev.next();
+ } else {
+ $child = $el.children().first();
+ }
+ }
+ };
+
+ /**
+ * Adds a whitespace to block elements. This is useful if you want to convert the contents
+ * to text and don't want words that are visually separate (e.g. table cells) to be fused.
+ * Works in-place.
+ *
+ * @param {jQuery} $el
+ */
+ HUP.appendWhitespaceToBlockElements = function ( $el ) {
+ // the list of what elements to add whitespace to is somewhat ad-hoc (not all of these
+ // are technically block-level elements, and a lot of block-level elements are missing)
+ // but will hopefully cover the common cases where text is fused together.
+ $el
+ .find( 'blockquote, dd, dl, dt, li, td' )
+ .before( ' ' )
+ .after( ' ' );
+ $el
+ .find( 'br, tr, p' )
+ .before( '\n' )
+ .after( '\n' );
+ };
+
+ /**
+ * Returns the HTML code for a jQuery element (only the first one if passed a set of elements).
+ * Unlike .html(), this includes HTML code for the outermost element; compare
+ * - `$('<div>').html() // ''`
+ * - `mw.mmv.HtmlUtils.jqueryToHtml( $('<div>') ) // '<div></div>'`
+ *
+ * @param {jQuery} $el
+ * @return {string}
+ */
+ HUP.jqueryToHtml = function ( $el ) {
+ // There are two possible implementations for this:
+ // 1) load innto a wrapper element and get its innerHTML;
+ // 2) use outerHTML.
+ // We go with 1) because it handles the case when a jQuery object contains something
+ // that is not an element (this can happen with e.g. $x.children() which returns text
+ // nodes as well).
+ return $( '<div>' ).append( $el ).html();
+ };
+
+ /**
+ * Cleans up superfluous whitespace.
+ * Given that the results will be displayed in a HTML environment, this doesn't have any real
+ * effect. It is mostly there to make testing easier.
+ *
+ * @protected
+ * @param {string} html a HTML (or plaintext) string
+ * @return {string}
+ */
+ HUP.mergeWhitespace = function ( html ) {
+ html = html.replace( /^\s+|\s+$/g, '' );
+ html = html.replace( /\s*\n\s*/g, '\n' );
+ html = html.replace( / {2,}/g, ' ' );
+ return html;
+ };
+
+ /**
+ * Returns the text content of a html string.
+ * Tries to give an approximation of what would be visible if the HTML would be displayed.
+ *
+ * @param {string} html
+ * @return {string}
+ */
+ HUP.htmlToText = function ( html ) {
+ var $html;
+ if ( !cache.text[ html ] ) {
+ $html = this.wrapAndJquerify( html );
+ this.filterInvisible( $html );
+ this.appendWhitespaceToBlockElements( $html );
+ cache.text[ html ] = this.mergeWhitespace( $html.text() );
+ }
+ return cache.text[ html ];
+ };
+
+ /**
+ * Returns the text content of a html string, with the `<a>`, `<i>`, `<b>` tags left intact.
+ * Tries to give an approximation of what would be visible if the HTML would be displayed.
+ *
+ * @param {string} html
+ * @return {string}
+ */
+ HUP.htmlToTextWithTags = function ( html ) {
+ var $html;
+ if ( !cache.textWithTags[ html ] ) {
+ $html = this.wrapAndJquerify( html );
+ this.filterInvisible( $html );
+ this.appendWhitespaceToBlockElements( $html );
+ this.whitelistHtml( $html, 'a, span, i, b, sup, sub' );
+ cache.textWithTags[ html ] = this.mergeWhitespace( $html.html() );
+ }
+ return cache.textWithTags[ html ];
+ };
+
+ /**
+ * Returns the text content of a html string, with the `<a>` tags left intact.
+ * Tries to give an approximation of what would be visible if the HTML would be displayed.
+ *
+ * @param {string} html
+ * @return {string}
+ */
+ HUP.htmlToTextWithLinks = function ( html ) {
+ var $html;
+ if ( !cache.textWithLinks[ html ] ) {
+ $html = this.wrapAndJquerify( html );
+ this.filterInvisible( $html );
+ this.appendWhitespaceToBlockElements( $html );
+ this.whitelistHtml( $html, 'a, span' );
+ cache.textWithLinks[ html ] = this.mergeWhitespace( $html.html() );
+ }
+ return cache.textWithLinks[ html ];
+ };
+
+ /**
+ * Generates HTML code for a link.
+ *
+ * @param {string} text Link text (plain text; will be sanitized)
+ * @param {Object} props Link attributes (should at a minumum include href; will be sanitized)
+ * @return {string}
+ */
+ HUP.makeLinkText = function ( text, props ) {
+ var key;
+ for ( key in props ) {
+ if ( !props.hasOwnProperty( key ) ) {
+ continue;
+ }
+ props[ key ] = this.htmlToText( props[ key ] );
+ }
+ return this.jqueryToHtml( $( '<a>' ).prop( props ).text( text ) );
+ };
+
+ mw.mmv.HtmlUtils = HtmlUtils;
+}( mediaWiki, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.ThumbnailWidthCalculator.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.ThumbnailWidthCalculator.js
new file mode 100644
index 00000000..2087aa4c
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.ThumbnailWidthCalculator.js
@@ -0,0 +1,172 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $ ) {
+ var TWCP;
+
+ /**
+ * A helper class for bucketing image sizes.
+ * Bucketing helps to avoid cache fragmentation and thus speed up image loading:
+ * instead of generating potentially hundreds of different thumbnail sizes, we restrict
+ * ourselves to a short list of acceptable thumbnail widths, and only ever load thumbnails
+ * of that size. Final size adjustment is done in a thumbnail.
+ *
+ * See also the [Standardized thumbnail sizes RFC][1].
+ *
+ * [1]: https://www.mediawiki.org/wiki/Talk:Requests_for_comment/Standardized_thumbnails_sizes
+ *
+ * @class mw.mmv.ThumbnailWidthCalculator
+ * @constructor
+ * @param {Object} [options]
+ * @param {number[]} [options.widthBuckets] see {@link mw.mmv.ThumbnailWidthCalculator#widthBuckets}
+ * @param {number} [options.devicePixelRatio] see {@link mw.mmv.ThumbnailWidthCalculator#devicePixelRatio};
+ * will be autodetected if omitted
+ */
+ function ThumbnailWidthCalculator( options ) {
+ options = $.extend( {}, this.defaultOptions, options );
+
+ if ( !options.widthBuckets.length ) {
+ throw new Error( 'No buckets!' );
+ }
+
+ /**
+ * List of thumbnail width bucket sizes, in pixels.
+ * @property {number[]}
+ */
+ this.widthBuckets = options.widthBuckets;
+ this.widthBuckets.sort( function ( a, b ) { return a - b; } );
+
+ /**
+ * Screen pixel count per CSS pixel.
+ * @property {number}
+ */
+ this.devicePixelRatio = options.devicePixelRatio;
+ }
+
+ TWCP = ThumbnailWidthCalculator.prototype;
+
+ /**
+ * The default list of image widths
+ * @static
+ * @property {Object}
+ */
+ TWCP.defaultOptions = {
+ // default image widths
+ widthBuckets: [
+ 320,
+ 800,
+ 1024,
+ 1280,
+ 1920,
+ 2560,
+ 2880
+ ],
+
+ // screen pixel per CSS pixel
+ devicePixelRatio: $.devicePixelRatio()
+ };
+
+ /**
+ * Finds the smallest bucket which is large enough to hold the target size
+ * (i. e. the smallest bucket whose size is equal to or greater than the target).
+ * If none of the buckets are large enough, returns the largest bucket.
+ *
+ * @param {number} target
+ * @return {number}
+ */
+ TWCP.findNextBucket = function ( target ) {
+ var i, bucket,
+ buckets = this.widthBuckets;
+
+ for ( i = 0; i < buckets.length; i++ ) {
+ bucket = buckets[ i ];
+
+ if ( bucket >= target ) {
+ return bucket;
+ }
+ }
+
+ // If we failed to find a high enough size...good luck
+ return bucket;
+ };
+
+ /**
+ * Finds the largest width for an image so that it will still fit into a given bounding box,
+ * based on the size of a sample (some smaller version of the same image, like the thumbnail
+ * shown in the article) which is used to calculate the ratio.
+ *
+ * This is for internal use, you should probably use calculateWidths() instead.
+ *
+ * @protected
+ * @param {number} boundingWidth width of the bounding box
+ * @param {number} boundingHeight height of the bounding box
+ * @param {number} sampleWidth width of the sample image
+ * @param {number} sampleHeight height of the sample image
+ * @return {number} the largest width so that the scaled version of the sample image fits
+ * into the bounding box (either horizontal or vertical edges touch on both sides).
+ */
+ TWCP.calculateFittingWidth = function ( boundingWidth, boundingHeight, sampleWidth, sampleHeight ) {
+ if ( ( boundingWidth / boundingHeight ) > ( sampleWidth / sampleHeight ) ) {
+ // we are limited by height; we need to calculate the max width that fits
+ return Math.round( ( sampleWidth / sampleHeight ) * boundingHeight );
+ } else {
+ // simple case, ratio tells us we're limited by width
+ return boundingWidth;
+ }
+ };
+
+ /**
+ * Finds the largest width for an image so that it will still fit into a given bounding box,
+ * based on the size of a sample (some smaller version of the same image, like the thumbnail
+ * shown in the article) which is used to calculate the ratio.
+ *
+ * Returns two values, a CSS width which is the size in pixels that should be used so the image
+ * fits exactly into the bounding box, and a real width which should be the size of the
+ * downloaded image in pixels. The two will be different for two reasons:
+ * - Images are bucketed for more efficient caching, so the real width will always be one of
+ * the numbers in this.widthBuckets. The resulting thumbnail will be slightly larger than
+ * the bounding box so that it takes roughly the same amount of bandwidth and
+ * looks decent when resized by the browser.
+ * - For devices with high pixel density (multiple actual pixels per CSS pixel) we want to use
+ * a larger image so that there will be roughly one image pixel per physical display pixel.
+ *
+ * @param {number} boundingWidth width of the bounding box, in CSS pixels
+ * @param {number} boundingHeight height of the bounding box, in CSS pixels
+ * @param {number} sampleWidth width of the sample image (in whatever - only used for aspect ratio)
+ * @param {number} sampleHeight height of the sample image (in whatever - only used for aspect ratio)
+ * @return {mw.mmv.model.ThumbnailWidth}
+ */
+
+ TWCP.calculateWidths = function ( boundingWidth, boundingHeight, sampleWidth, sampleHeight ) {
+ var cssWidth,
+ cssHeight,
+ screenPixelWidth,
+ bucketedWidth,
+ ratio = sampleHeight / sampleWidth;
+
+ cssWidth = this.calculateFittingWidth( boundingWidth, boundingHeight, sampleWidth, sampleHeight );
+ cssHeight = Math.round( cssWidth * ratio );
+
+ screenPixelWidth = cssWidth * this.devicePixelRatio;
+
+ bucketedWidth = this.findNextBucket( screenPixelWidth );
+
+ return new mw.mmv.model.ThumbnailWidth( cssWidth, cssHeight, screenPixelWidth, bucketedWidth );
+ };
+
+ mw.mmv.ThumbnailWidthCalculator = ThumbnailWidthCalculator;
+}( mediaWiki, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.base.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.base.js
new file mode 100644
index 00000000..400438a3
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.base.js
@@ -0,0 +1,34 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+// Included on every page which has images so keep it lightweight.
+( function ( mw ) {
+ mw.mmv = {
+ /**
+ * Feature-detects SVG support. MuyltimediaViewer uses SVG icons extensively and is
+ * unusable without them.
+ *
+ * @member mw.mmv.MultimediaViewer
+ * @return {boolean}
+ */
+ isBrowserSupported: function () {
+ // From modernizr 2.6.1
+ var ns = { svg: 'http://www.w3.org/2000/svg' };
+ return !!document.createElementNS && !!document.createElementNS( ns.svg, 'svg' ).createSVGRect;
+ }
+ };
+}( mediaWiki ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.bootstrap.autostart.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.bootstrap.autostart.js
new file mode 100644
index 00000000..7fde43f3
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.bootstrap.autostart.js
@@ -0,0 +1,34 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+// This file is used to do the global initialization that we want on the real pages,
+// but do not want in the tests.
+( function ( mw, $ ) {
+ var bootstrap;
+
+ if ( !mw.mmv.isBrowserSupported() ) {
+ return;
+ }
+
+ bootstrap = new mw.mmv.MultimediaViewerBootstrap();
+
+ $( function () {
+ bootstrap.setupEventHandlers();
+ } );
+
+ mw.mmv.bootstrap = bootstrap;
+}( mediaWiki, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.bootstrap.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.bootstrap.js
new file mode 100644
index 00000000..1165ca02
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.bootstrap.js
@@ -0,0 +1,631 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $ ) {
+ var MMVB;
+
+ /**
+ * Bootstrap code listening to thumb clicks checking the initial location.hash
+ * Loads the mmv and opens it if necessary
+ *
+ * @class mw.mmv.MultimediaViewerBootstrap
+ */
+ function MultimediaViewerBootstrap() {
+ // Exposed for tests
+ this.hoverWaitDuration = 200;
+
+ // TODO lazy-load config and htmlUtils
+
+ /** @property {mw.mmv.Config} config - */
+ this.config = new mw.mmv.Config(
+ mw.config.get( 'wgMultimediaViewer', {} ),
+ mw.config,
+ mw.user,
+ new mw.Api(),
+ mw.storage
+ );
+
+ this.validExtensions = this.config.extensions();
+
+ /** @property {mw.mmv.HtmlUtils} htmlUtils - */
+ this.htmlUtils = new mw.mmv.HtmlUtils();
+
+ /**
+ * This flag is set to true when we were unable to load the viewer.
+ * @property {boolean}
+ */
+ this.viewerIsBroken = false;
+
+ this.thumbsReadyDeferred = $.Deferred();
+ this.thumbs = [];
+ this.$thumbs = null; // will be set by processThumbs
+
+ // find and setup all thumbs on this page
+ // this will run initially and then every time the content changes,
+ // e.g. via a VE edit or pagination in a multipage file
+ mw.hook( 'wikipage.content' ).add( $.proxy( this, 'processThumbs' ) );
+
+ this.browserHistory = window.history;
+ }
+
+ MMVB = MultimediaViewerBootstrap.prototype;
+
+ /**
+ * Loads the mmv module asynchronously and passes the thumb data to it
+ *
+ * @param {boolean} [setupOverlay]
+ * @return {jQuery.Promise}
+ */
+ MMVB.loadViewer = function ( setupOverlay ) {
+ var deferred = $.Deferred(),
+ bs = this,
+ viewer,
+ message;
+
+ // Don't load if someone has specifically stopped us from doing so
+ if ( mw.config.get( 'wgMediaViewer' ) !== true ) {
+ return deferred.reject();
+ }
+
+ // FIXME setupOverlay is a quick hack to avoid setting up and immediately
+ // removing the overlay on a not-MMV -> not-MMV hash change.
+ // loadViewer is called on every click and hash change and setting up
+ // the overlay is not needed on all of those; this logic really should
+ // not be here.
+ if ( setupOverlay ) {
+ bs.setupOverlay();
+ }
+
+ mw.loader.using( 'mmv', function () {
+ try {
+ viewer = bs.getViewer();
+ } catch ( e ) {
+ message = e.message;
+ if ( e.stack ) {
+ message += '\n' + e.stack;
+ }
+ deferred.reject( message );
+ return;
+ }
+ deferred.resolve( viewer );
+ }, function ( error ) {
+ deferred.reject( error.message );
+ } );
+
+ return deferred.promise()
+ .then(
+ function ( viewer ) {
+ if ( !bs.viewerInitialized ) {
+ if ( bs.thumbs.length ) {
+ viewer.initWithThumbs( bs.thumbs );
+ }
+
+ bs.viewerInitialized = true;
+ }
+ return viewer;
+ },
+ function ( message ) {
+ mw.log.warn( message );
+ bs.cleanupOverlay();
+ bs.viewerIsBroken = true;
+ mw.notify( 'Error loading MediaViewer: ' + message );
+ return $.Deferred().reject( message );
+ }
+ );
+ };
+
+ /**
+ * Processes all thumbs found on the page
+ *
+ * @param {jQuery} $content Element to search for thumbs
+ */
+ MMVB.processThumbs = function ( $content ) {
+ var bs = this;
+
+ this.$thumbs = $content.find(
+ '.gallery .image img, ' +
+ 'a.image img, ' +
+ '#file a img, ' +
+ 'figure[typeof*="mw:Image"] > *:first-child > img, ' +
+ 'span[typeof*="mw:Image"] img'
+ );
+
+ try {
+ this.$thumbs.each( function ( i, thumb ) {
+ bs.processThumb( thumb );
+ } );
+ } finally {
+ this.thumbsReadyDeferred.resolve();
+ // now that we have set up our real click handler we can we can remove the temporary
+ // handler added in mmv.head.js which just replays clicks to the real handler
+ $( document ).off( 'click.mmv-head' );
+ }
+ };
+
+ /**
+ * Check if this thumbnail should be handled by MediaViewer
+ *
+ * @param {jQuery} $thumb the thumbnail (an `<img>` element) in question
+ * @return {boolean}
+ */
+ MMVB.isAllowedThumb = function ( $thumb ) {
+ var selectors = [
+ '.metadata', // this is inside an informational template like {{refimprove}} on enwiki.
+ '.noviewer', // MediaViewer has been specifically disabled for this image
+ '.noarticletext', // we are on an error page for a non-existing article, the image is part of some template
+ '#siteNotice',
+ 'ul.mw-gallery-slideshow li.gallerybox' // thumbnails of a slideshow gallery
+ ];
+ return $thumb.closest( selectors.join( ', ' ) ).length === 0;
+
+ };
+
+ /**
+ * Processes a thumb
+ *
+ * @param {Object} thumb
+ */
+ MMVB.processThumb = function ( thumb ) {
+ var title,
+ bs = this,
+ $thumb = $( thumb ),
+ $link = $thumb.closest( 'a.image, [typeof*="mw:Image"] > a' ),
+ $thumbContain = $link.closest( '.thumb, [typeof*="mw:Image"]' ),
+ $enlarge = $thumbContain.find( '.magnify a' ),
+ link = $link.prop( 'href' ),
+ alt = $thumb.attr( 'alt' ),
+ isFilePageMainThumb = $thumb.closest( '#file' ).length > 0;
+
+ if ( isFilePageMainThumb ) {
+ // main thumbnail (file preview area) of a file page
+ // if this is a PDF filetype thumbnail, it can trick us,
+ // so we short-circuit that logic and use the file page title
+ // instead of the thumbnail logic.
+ title = mw.Title.newFromText( mw.config.get( 'wgTitle' ), mw.config.get( 'wgNamespaceNumber' ) );
+ } else {
+ title = mw.Title.newFromImg( $thumb );
+ }
+
+ if ( !title || !title.getExtension() || !( title.getExtension().toLowerCase() in bs.validExtensions ) ) {
+ // Short-circuit event handler and interface setup, because
+ // we can't do anything for this filetype
+ return;
+ }
+
+ if ( !bs.isAllowedThumb( $thumb ) ) {
+ return;
+ }
+
+ if ( $thumbContain.length ) {
+ // If this is a thumb, we preload JS/CSS when the mouse cursor hovers the thumb container (thumb image + caption + border)
+ $thumbContain.mouseenter( function () {
+ // There is no point preloading if clicking the thumb won't open Media Viewer
+ if ( !bs.config.isMediaViewerEnabledOnClick() ) {
+ return;
+ }
+ bs.preloadOnHoverTimer = setTimeout( function () {
+ mw.loader.load( 'mmv' );
+ }, bs.hoverWaitDuration );
+ } ).mouseleave( function () {
+ if ( bs.preloadOnHoverTimer ) {
+ clearTimeout( bs.preloadOnHoverTimer );
+ }
+ } );
+ }
+
+ if ( isFilePageMainThumb ) {
+ this.processFilePageThumb( $thumb, title );
+ return;
+ }
+
+ // This is the data that will be passed onto the mmv
+ this.thumbs.push( {
+ thumb: thumb,
+ $thumb: $thumb,
+ title: title,
+ link: link,
+ alt: alt,
+ caption: this.findCaption( $thumbContain, $link ) } );
+
+ $link.add( $enlarge ).click( function ( e ) {
+ return bs.click( this, e, title );
+ } );
+ };
+
+ /**
+ * Processes the main thumbnail of a file page by adding some buttons
+ * below to open MediaViewer.
+ *
+ * @param {jQuery} $thumb
+ * @param {mw.Title} title
+ */
+ MMVB.processFilePageThumb = function ( $thumb, title ) {
+ var $link,
+ $configLink,
+ $filepageButtons,
+ bs = this,
+ link = $thumb.closest( 'a' ).prop( 'href' );
+
+ // remove the buttons (and the clearing element) if they are already there
+ // this should not happen (at least until we support paged media) but just in case
+ $( '.mw-mmv-filepage-buttons' ).next().addBack().remove();
+
+ $link = $( '<a>' )
+ // It won't matter because we catch the click event anyway, but
+ // give the user some URL to see.
+ .prop( 'href', link )
+ .addClass( 'mw-mmv-view-expanded mw-ui-button mw-ui-icon mw-ui-icon-before' )
+ .text( mw.message( 'multimediaviewer-view-expanded' ).text() );
+
+ $configLink = $( '<a>' )
+ .prop( 'href', $thumb.closest( 'a' ).prop( 'href' ) )
+ .addClass( 'mw-mmv-view-config mw-ui-button mw-ui-icon mw-ui-icon-element' )
+ .text( mw.message( 'multimediaviewer-view-config' ).text() );
+
+ $filepageButtons = $( '<div>' )
+ .addClass( 'mw-ui-button-group mw-mmv-filepage-buttons' )
+ .append( $link, $configLink );
+
+ $( '.fullMedia' ).append(
+ $filepageButtons,
+ $( '<div>' )
+ .css( 'clear', 'both' )
+ );
+
+ this.thumbs.push( {
+ thumb: $thumb.get( 0 ),
+ $thumb: $thumb,
+ title: title,
+ link: link
+ } );
+
+ $link.click( function () {
+ if ( bs.statusInfoDialog ) {
+ bs.statusInfoDialog.close();
+ }
+ bs.openImage( this, title );
+ return false;
+ } );
+
+ $configLink.click( function () {
+ if ( bs.statusInfoDialog ) {
+ bs.statusInfoDialog.close();
+ }
+ bs.openImage( this, title ).then( function () {
+ $( document ).trigger( 'mmv-options-open' );
+ } );
+ return false;
+ } );
+
+ if ( this.config.shouldShowStatusInfo() ) {
+ this.config.disableStatusInfo();
+ this.showStatusInfo();
+ }
+ };
+
+ /**
+ * Shows a popup notifying the user
+ */
+ MMVB.showStatusInfo = function () {
+ var bs = this;
+
+ mw.loader.using( 'mmv.ui.tipsyDialog' ).done( function () {
+ /** @property {mw.mmv.ui.TipsyDialog} statusInfoDialog popup on the file page explaining how to re-enable */
+ bs.statusInfoDialog = new mw.mmv.ui.TipsyDialog( $( '.mw-mmv-view-expanded' ), { gravity: 'sw' } );
+ bs.statusInfoDialog.setContent(
+ mw.message( 'multimediaviewer-disable-info-title' ).plain(),
+ mw.message( 'multimediaviewer-disable-info' ).escaped()
+ );
+ // tipsy mispositions the tooltip, probably because it does the positioning before the buttons are
+ // displayed and the page is reflown. Adding some delay seems to help.
+ window.setTimeout( function () {
+ bs.statusInfoDialog.open();
+ }, 1000 );
+ } );
+ };
+
+ /**
+ * Finds the caption for an image.
+ *
+ * @param {jQuery} $thumbContain The container for the thumbnail.
+ * @param {jQuery} $link The link that encompasses the thumbnail.
+ * @return {string|undefined} Unsafe HTML may be present - caution
+ */
+ MMVB.findCaption = function ( $thumbContain, $link ) {
+ var $thumbCaption, $potentialCaptions;
+
+ if ( !$thumbContain.length ) {
+ return $link.prop( 'title' ) || undefined;
+ }
+
+ $potentialCaptions = $thumbContain.find( '.thumbcaption, figcaption' );
+ if ( $potentialCaptions.length < 2 ) {
+ $thumbCaption = $potentialCaptions.eq( 0 );
+ } else {
+ // Template:Multiple_image or some such; try to find closest caption to the image
+ $thumbCaption = $link.closest( ':has(> .thumbcaption)', $thumbContain )
+ .find( '> .thumbcaption' );
+ }
+
+ if ( !$thumbCaption.length ) { // gallery, maybe
+ $thumbCaption = $thumbContain
+ .closest( '.gallerybox' )
+ .not( function () {
+ // do not treat categories as galleries - the autogenerated caption they have is not helpful
+ return $thumbContain.closest( '#mw-category-media' ).length;
+ } )
+ .not( function () {
+ // do not treat special file related pages as galleries
+ var $specialFileRelatedPages = $(
+ '.page-Special_NewFiles, ' +
+ '.page-Special_MostLinkedFiles,' +
+ '.page-Special_MostGloballyLinkedFiles, ' +
+ '.page-Special_UncategorizedFiles, ' +
+ '.page-Special_UnusedFiles'
+ );
+ return $thumbContain.closest( $specialFileRelatedPages ).length;
+ } )
+ .find( '.gallerytext' );
+ }
+
+ if ( $thumbCaption.find( '.magnify' ).length ) {
+ $thumbCaption = $thumbCaption.clone();
+ $thumbCaption.find( '.magnify' ).remove();
+ }
+
+ return this.htmlUtils.htmlToTextWithTags( $thumbCaption.html() || '' );
+ };
+
+ /**
+ * Opens MediaViewer and loads the given thumbnail. Requires processThumb() to be called first.
+ *
+ * @param {HTMLElement} element Clicked element
+ * @param {string} title File title
+ * @return {jQuery.Promise}
+ */
+ MMVB.openImage = function ( element, title ) {
+ var $element = $( element );
+
+ mw.mmv.durationLogger.start( [ 'click-to-first-image', 'click-to-first-metadata' ] );
+
+ if ( $element.is( 'a.image, [typeof*="mw:Image"] > a' ) ) {
+ mw.mmv.actionLogger.log( 'thumbnail' );
+ } else if ( $element.is( '.magnify a' ) ) {
+ mw.mmv.actionLogger.log( 'enlarge' );
+ }
+
+ this.ensureEventHandlersAreSetUp();
+
+ return this.loadViewer( true ).then( function ( viewer ) {
+ viewer.loadImageByTitle( title, true );
+ } );
+ };
+
+ /**
+ * Handles a click event on a link
+ *
+ * @param {HTMLElement} element Clicked element
+ * @param {jQuery.Event} e jQuery event object
+ * @param {string} title File title
+ * @return {boolean} a value suitable for an event handler (ie. true if the click should be handled
+ * by the browser).
+ */
+ MMVB.click = function ( element, e, title ) {
+ // Do not interfere with non-left clicks or if modifier keys are pressed.
+ if ( ( e.button !== 0 && e.which !== 1 ) || e.altKey || e.ctrlKey || e.shiftKey || e.metaKey ) {
+ return true;
+ }
+
+ // Don't load if someone has specifically stopped us from doing so
+ if ( !this.config.isMediaViewerEnabledOnClick() ) {
+ return true;
+ }
+
+ // Don't load if we already tried loading and it failed
+ if ( this.viewerIsBroken ) {
+ return true;
+ }
+
+ this.openImage( element, title );
+
+ // calling this late so that in case of errors users at least get to the file page
+ e.preventDefault();
+
+ return false;
+ };
+
+ /**
+ * Returns true if the hash part of the current URL is one that's owned by MMV.
+ *
+ * @return {boolean}
+ * @private
+ */
+ MMVB.isViewerHash = function () {
+ return window.location.hash.indexOf( '#mediaviewer/' ) === 0 ||
+ window.location.hash.indexOf( '#/media/' ) === 0;
+ };
+
+ /**
+ * Handles the browser location hash on pageload or hash change
+ *
+ * @param {boolean} initialHash Whether this is called for the hash that came with the pageload
+ */
+ MMVB.hash = function ( initialHash ) {
+ var bootstrap = this;
+
+ // There is no point loading the mmv if it isn't loaded yet for hash changes unrelated to the mmv
+ // Such as anchor links on the page
+ if ( !this.viewerInitialized && !this.isViewerHash() ) {
+ return;
+ }
+
+ if ( this.skipNextHashHandling ) {
+ this.skipNextHashHandling = false;
+ return;
+ }
+
+ this.loadViewer( this.isViewerHash() ).then( function ( viewer ) {
+ viewer.hash();
+ // this is an ugly temporary fix to avoid a black screen of death when
+ // the page is loaded with an invalid MMV url
+ if ( !viewer.isOpen ) {
+ bootstrap.cleanupOverlay();
+ } else if ( initialHash ) {
+ mw.mmv.actionLogger.log( 'hash-load' );
+ } else {
+ mw.mmv.actionLogger.log( 'history-navigation' );
+ }
+ } );
+ };
+
+ /**
+ * Handles hash change requests coming from mmv
+ *
+ * @param {jQuery.Event} e Custom mmv-hash event
+ */
+ MMVB.internalHashChange = function ( e ) {
+ var hash = e.hash,
+ title = e.title;
+
+ // The advantage of using pushState when it's available is that it has to ability to truly
+ // clear the hash, not leaving "#" in the history
+ // An entry with "#" in the history has the side-effect of resetting the scroll position when navigating the history
+ if ( this.browserHistory && this.browserHistory.pushState ) {
+ // In order to truly clear the hash, we need to reconstruct the hash-free URL
+ if ( hash === '#' ) {
+ hash = window.location.href.replace( /#.*$/, '' );
+ }
+
+ window.history.pushState( null, title, hash );
+ } else {
+ // Since we voluntarily changed the hash, we don't want MMVB.hash (which will trigger on hashchange event) to treat it
+ this.skipNextHashHandling = true;
+
+ window.location.hash = hash;
+ }
+
+ document.title = title;
+ };
+
+ /**
+ * Instantiates a new viewer if necessary
+ *
+ * @return {mw.mmv.MultimediaViewer}
+ */
+ MMVB.getViewer = function () {
+ if ( this.viewer === undefined ) {
+ this.viewer = new mw.mmv.MultimediaViewer( this.config );
+ this.viewer.setupEventHandlers();
+ mw.mmv.viewer = this.viewer;
+ }
+
+ return this.viewer;
+ };
+
+ /**
+ * Listens to events on the window/document
+ */
+ MMVB.setupEventHandlers = function () {
+ var self = this;
+
+ /** @property {boolean} eventHandlersHaveBeenSetUp tracks domready event handler state */
+ this.eventHandlersHaveBeenSetUp = true;
+
+ $( window ).on( this.browserHistory && this.browserHistory.pushState ? 'popstate.mmvb' : 'hashchange', function () {
+ self.hash();
+ } );
+
+ // Interpret any hash that might already be in the url
+ self.hash( true );
+
+ $( document ).on( 'mmv-hash', function ( e ) {
+ self.internalHashChange( e );
+ } ).on( 'mmv-cleanup-overlay', function () {
+ self.cleanupOverlay();
+ } );
+ };
+
+ /**
+ * Cleans up event handlers, used for tests
+ */
+ MMVB.cleanupEventHandlers = function () {
+ $( window ).off( 'hashchange popstate.mmvb' );
+ $( document ).off( 'mmv-hash' );
+ this.eventHandlersHaveBeenSetUp = false;
+ };
+
+ /**
+ * Makes sure event handlers are set up properly via MultimediaViewerBootstrap.setupEventHandlers().
+ * Called before loading the main mmv module. At this point, event handers for MultimediaViewerBootstrap
+ * should have been set up, but due to bug 70756 it cannot be guaranteed.
+ */
+ MMVB.ensureEventHandlersAreSetUp = function () {
+ if ( !this.eventHandlersHaveBeenSetUp ) {
+ this.setupEventHandlers();
+ }
+ };
+
+ /**
+ * Sets up the overlay while the viewer loads
+ */
+ MMVB.setupOverlay = function () {
+ var $body = $( document.body );
+
+ // There are situations where we can call setupOverlay while the overlay is already there,
+ // such as inside this.hash(). In that case, do nothing
+ if ( $body.hasClass( 'mw-mmv-lightbox-open' ) ) {
+ return;
+ }
+
+ if ( !this.$overlay ) {
+ this.$overlay = $( '<div>' )
+ .addClass( 'mw-mmv-overlay' );
+ }
+
+ this.savedScrollTop = $( window ).scrollTop();
+
+ $body.addClass( 'mw-mmv-lightbox-open' )
+ .append( this.$overlay );
+ };
+
+ /**
+ * Cleans up the overlay
+ */
+ MMVB.cleanupOverlay = function () {
+ var bootstrap = this;
+
+ $( document.body ).removeClass( 'mw-mmv-lightbox-open' );
+
+ if ( this.$overlay ) {
+ this.$overlay.remove();
+ }
+
+ if ( this.savedScrollTop !== undefined ) {
+ // setTimeout because otherwise Chrome will scroll back to top after the popstate event handlers run
+ setTimeout( function () {
+ $( window ).scrollTop( bootstrap.savedScrollTop );
+ bootstrap.savedScrollTop = undefined;
+ } );
+ }
+ };
+
+ MMVB.whenThumbsReady = function () {
+ return this.thumbsReadyDeferred.promise();
+ };
+
+ mw.mmv.MultimediaViewerBootstrap = MultimediaViewerBootstrap;
+}( mediaWiki, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.bootstrap.less b/www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.bootstrap.less
new file mode 100644
index 00000000..82547243
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.bootstrap.less
@@ -0,0 +1,69 @@
+.mw-mmv-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ z-index: 1000;
+ background-color: #000;
+}
+
+body.mw-mmv-lightbox-open {
+ overflow-y: auto;
+
+ /* stylelint-disable selector-max-id */
+ #mw-page-base,
+ #mw-head-base,
+ #mw-navigation,
+ #content,
+ #footer,
+ #globalWrapper { // monobook
+ /** Stop the article from scrolling in the background - skin-specific but works in any browser */
+ display: none;
+ }
+ /* stylelint-enable selector-max-id */
+
+ > * {
+ /** Stop the article from scrolling in the background - works with any skin but needs modern browser */
+ display: none;
+ }
+ > .mw-mmv-overlay,
+ > .mw-mmv-wrapper {
+ display: block;
+ }
+}
+
+.mw-mmv-filepage-buttons {
+ margin-top: 5px;
+
+ .mw-mmv-view-expanded,
+ .mw-mmv-view-config {
+ display: block;
+ // Work around some weirdness of MW-UI buttons. T127052
+ line-height: inherit;
+ }
+
+ .mw-mmv-view-expanded.mw-ui-icon:before {
+ /* @embed */
+ background-image: url( img/expand.svg );
+ }
+
+ .mw-mmv-view-config.mw-ui-icon:before {
+ /* @embed */
+ background-image: url( img/gear.svg );
+ opacity: 0.75;
+
+ &:hover {
+ opacity: 1;
+ }
+ }
+}
+
+.mw-mmv-button {
+ background-color: transparent;
+ min-width: 0;
+ border: 0;
+ padding: 0;
+ overflow-x: hidden;
+ text-indent: -9999em;
+}
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.globals.less b/www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.globals.less
new file mode 100644
index 00000000..2a4aec1c
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.globals.less
@@ -0,0 +1,31 @@
+// Height of the area of the metadata bar which is visible without scrolling.
+@metadatabar-above-fold-height: 86px;
+
+// adjust for @metadatabar-below-fold-pushup-height wide bottom padding - that area will be overlapped
+// by the revealed part of the below-the-fold content
+@metadatabar-above-fold-inner-height: @metadatabar-above-fold-height - @metadatabar-below-fold-pushup-height;
+
+// Height of the top part of the "below-fold" content which is actually above the fold, as a scrolling affordance
+@metadatabar-below-fold-pushup-height: 30px;
+
+@panel-above-fold-background-color: #fff;
+
+// Height of the progress bar
+@progress-bar-height: 14px;
+
+// Height of dialogs
+@dialog-height: 350px;
+
+// Border radius for dialogs
+@border-radius: 2px;
+
+// Arrow size for dialogs
+@arrow-size: 20px;
+@arrow-border-size: 2px;
+
+@dialog-warning-color: #ffd36e;
+
+// Some button things that get included all over
+@navbutton-width: 18px;
+@buttons-offset-right: 5px;
+@buttons-offset-each-top: 37px;
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.head.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.head.js
new file mode 100644
index 00000000..069309b5
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.head.js
@@ -0,0 +1,59 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $ ) {
+ var $document = $( document ),
+ start;
+
+ if ( !mw.mmv.isBrowserSupported() ) {
+ return;
+ }
+
+ // If the user disabled MediaViewer in his preferences, we do not set up click handling.
+ // This is loaded before user JS so we cannot check wgMediaViewer.
+ if (
+ mw.config.get( 'wgMediaViewerOnClick' ) !== true ||
+ mw.user.isAnon() && mw.storage.get( 'wgMediaViewerOnClick', '1' ) !== '1'
+ ) {
+ return;
+ }
+
+ $document.on( 'click.mmv-head', 'a.image', function ( e ) {
+ // Do not interfere with non-left clicks or if modifier keys are pressed.
+ // Also, make sure we do not get in a loop.
+ if ( ( e.button !== 0 && e.which !== 1 ) || e.altKey || e.ctrlKey || e.shiftKey || e.metaKey || e.replayed ) {
+ return;
+ }
+
+ start = $.now();
+
+ // We wait for document readiness because mw.loader.using writes to the DOM
+ // which can cause a blank page if it happens before DOM readiness
+ $( function () {
+ mw.loader.using( [ 'mmv.bootstrap.autostart' ], function () {
+ mw.mmv.bootstrap.whenThumbsReady().then( function () {
+ mw.mmv.durationLogger.stop( 'early-click-to-replay-click', start ).record( 'early-click-to-replay-click' );
+
+ // We have to copy the properties, passing e doesn't work. Probably because of preventDefault()
+ $( e.target ).trigger( { type: 'click', which: 1, replayed: true } );
+ } );
+ } );
+ } );
+
+ e.preventDefault();
+ } );
+}( mediaWiki, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.js
new file mode 100644
index 00000000..8d164baa
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.js
@@ -0,0 +1,1031 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $ ) {
+ var MMVP,
+ comingFromHashChange = false;
+
+ /**
+ * Analyses the page, looks for image content and sets up the hooks
+ * to manage the viewing experience of such content.
+ *
+ * @class mw.mmv.MultimediaViewer
+ * @constructor
+ * @param {mw.mmv.Config} config mw.mmv.Config object
+ */
+ function MultimediaViewer( config ) {
+ var apiCacheMaxAge = 86400, // one day (24 hours * 60 min * 60 sec)
+ apiCacheFiveMinutes = 300; // 5 min * 60 sec
+
+ /**
+ * @property {mw.mmv.Config}
+ * @private
+ */
+ this.config = config;
+
+ /**
+ * @property {mw.mmv.provider.Image}
+ * @private
+ */
+ this.imageProvider = new mw.mmv.provider.Image( this.config.imageQueryParameter() );
+
+ /**
+ * @property {mw.mmv.provider.ImageInfo}
+ * @private
+ */
+ this.imageInfoProvider = new mw.mmv.provider.ImageInfo( new mw.mmv.logging.Api( 'imageinfo' ), {
+ language: this.config.language(),
+ maxage: apiCacheFiveMinutes
+ } );
+
+ /**
+ * @property {mw.mmv.provider.FileRepoInfo}
+ * @private
+ */
+ this.fileRepoInfoProvider = new mw.mmv.provider.FileRepoInfo( new mw.mmv.logging.Api( 'filerepoinfo' ),
+ { maxage: apiCacheMaxAge } );
+
+ /**
+ * @property {mw.mmv.provider.ThumbnailInfo}
+ * @private
+ */
+ this.thumbnailInfoProvider = new mw.mmv.provider.ThumbnailInfo( new mw.mmv.logging.Api( 'thumbnailinfo' ),
+ { maxage: apiCacheMaxAge } );
+
+ /**
+ * @property {mw.mmv.provider.ThumbnailInfo}
+ * @private
+ */
+ this.guessedThumbnailInfoProvider = new mw.mmv.provider.GuessedThumbnailInfo();
+
+ /**
+ * Image index on page.
+ * @property {number}
+ */
+ this.currentIndex = 0;
+
+ /**
+ * @property {mw.mmv.routing.Router} router -
+ */
+ this.router = new mw.mmv.routing.Router();
+
+ /**
+ * UI object used to display the pictures in the page.
+ * @property {mw.mmv.LightboxInterface}
+ * @private
+ */
+ this.ui = new mw.mmv.LightboxInterface();
+
+ /**
+ * How many sharp images have been displayed in Media Viewer since the pageload
+ * @property {number}
+ */
+ this.imageDisplayedCount = 0;
+
+ /**
+ * How many data-filled metadata panels have been displayed in Media Viewer since the pageload
+ * @property {number}
+ */
+ this.metadataDisplayedCount = 0;
+
+ /** @property {string} documentTitle base document title, MediaViewer will expand this */
+ this.documentTitle = document.title;
+
+ /**
+ * @property {mw.mmv.logging.ViewLogger} view -
+ */
+ this.viewLogger = new mw.mmv.logging.ViewLogger( this.config, window, mw.mmv.actionLogger );
+ }
+
+ MMVP = MultimediaViewer.prototype;
+
+ /**
+ * Initialize the lightbox interface given an array of thumbnail
+ * objects.
+ *
+ * @param {Object[]} thumbs Complex structure...TODO, document this better.
+ */
+ MMVP.initWithThumbs = function ( thumbs ) {
+ var i, thumb;
+
+ this.thumbs = thumbs;
+
+ for ( i = 0; i < this.thumbs.length; i++ ) {
+ thumb = this.thumbs[ i ];
+ // Create a LightboxImage object for each legit image
+ thumb.image = this.createNewImage(
+ thumb.$thumb.prop( 'src' ),
+ thumb.link,
+ thumb.title,
+ i,
+ thumb.thumb,
+ thumb.caption,
+ thumb.alt
+ );
+
+ thumb.extraStatsDeferred = $.Deferred();
+ }
+ };
+
+ /**
+ * Create an image object for the lightbox to use.
+ *
+ * @protected
+ * @param {string} fileLink Link to the file - generally a thumb URL
+ * @param {string} filePageLink Link to the File: page
+ * @param {mw.Title} fileTitle Represents the File: page
+ * @param {number} index Which number file this is
+ * @param {HTMLImageElement} thumb The thumbnail that represents this image on the page
+ * @param {string} [caption] The caption, if any.
+ * @param {string} [alt] The alt text of the image
+ * @return {mw.mmv.LightboxImage}
+ */
+ MMVP.createNewImage = function ( fileLink, filePageLink, fileTitle, index, thumb, caption, alt ) {
+ var thisImage = new mw.mmv.LightboxImage( fileLink, filePageLink, fileTitle, index, thumb, caption, alt ),
+ $thumb = $( thumb );
+
+ thisImage.filePageLink = filePageLink;
+ thisImage.filePageTitle = fileTitle;
+ thisImage.index = index;
+ thisImage.thumbnail = thumb;
+ thisImage.originalWidth = parseInt( $thumb.data( 'file-width' ), 10 );
+ thisImage.originalHeight = parseInt( $thumb.data( 'file-height' ), 10 );
+
+ return thisImage;
+ };
+
+ /**
+ * Handles resize events in viewer.
+ *
+ * @protected
+ * @param {mw.mmv.LightboxInterface} ui lightbox that got resized
+ */
+ MMVP.resize = function ( ui ) {
+ var imageWidths, canvasDimensions,
+ viewer = this,
+ image = this.thumbs[ this.currentIndex ].image,
+ ext = this.thumbs[ this.currentIndex ].title.ext.toLowerCase();
+
+ this.preloadThumbnails();
+
+ if ( image ) {
+ imageWidths = ui.canvas.getCurrentImageWidths();
+ canvasDimensions = ui.canvas.getDimensions();
+
+ mw.mmv.dimensionLogger.logDimensions( imageWidths, canvasDimensions, 'resize' );
+
+ this.fetchThumbnailForLightboxImage(
+ image, imageWidths.real
+ ).then( function ( thumbnail, image ) {
+ image.className = ext;
+ viewer.setImage( ui, thumbnail, image, imageWidths );
+ }, function ( error ) {
+ viewer.ui.canvas.showError( error );
+ } );
+ }
+
+ this.updateControls();
+ };
+
+ /**
+ * Updates positioning of controls, usually after a resize event.
+ */
+ MMVP.updateControls = function () {
+ var numImages = this.thumbs ? this.thumbs.length : 0,
+ showNextButton = this.currentIndex < ( numImages - 1 ),
+ showPreviousButton = this.currentIndex > 0;
+
+ this.ui.updateControls( showNextButton, showPreviousButton );
+ };
+
+ /**
+ * Loads and sets the specified image. It also updates the controls.
+ *
+ * @param {mw.mmv.LightboxInterface} ui image container
+ * @param {mw.mmv.model.Thumbnail} thumbnail thumbnail information
+ * @param {HTMLImageElement} imageElement
+ * @param {mw.mmv.model.ThumbnailWidth} imageWidths
+ */
+ MMVP.setImage = function ( ui, thumbnail, imageElement, imageWidths ) {
+ ui.canvas.setImageAndMaxDimensions( thumbnail, imageElement, imageWidths );
+ this.updateControls();
+ };
+
+ /**
+ * Loads a specified image.
+ *
+ * @param {mw.mmv.LightboxImage} image
+ * @param {HTMLImageElement} initialImage A thumbnail to use as placeholder while the image loads
+ */
+ MMVP.loadImage = function ( image, initialImage ) {
+ var imageWidths,
+ canvasDimensions,
+ imagePromise,
+ metadataPromise,
+ pluginsPromise,
+ start,
+ viewer = this,
+ $initialImage = $( initialImage ),
+ extraStatsDeferred = $.Deferred();
+
+ pluginsPromise = this.loadExtensionPlugins( image.filePageTitle.ext.toLowerCase() );
+
+ this.currentIndex = image.index;
+
+ this.currentImageFileTitle = image.filePageTitle;
+
+ if ( !this.isOpen ) {
+ this.ui.open();
+ this.isOpen = true;
+ } else {
+ this.ui.empty();
+ }
+ this.setHash();
+
+ // At this point we can't show the thumbnail because we don't
+ // know what size it should be. We still assign it to allow for
+ // size calculations in getCurrentImageWidths, which needs to know
+ // the aspect ratio
+ $initialImage.hide();
+ $initialImage.addClass( 'mw-mmv-placeholder-image' );
+ $initialImage.addClass( image.filePageTitle.ext.toLowerCase() );
+
+ this.ui.canvas.set( image, $initialImage );
+
+ this.preloadImagesMetadata();
+ this.preloadThumbnails();
+ // this.preloadFullscreenThumbnail( image ); // disabled - #474
+
+ imageWidths = this.ui.canvas.getCurrentImageWidths();
+ canvasDimensions = this.ui.canvas.getDimensions();
+
+ start = $.now();
+
+ mw.mmv.dimensionLogger.logDimensions( imageWidths, canvasDimensions, 'show' );
+
+ imagePromise = this.fetchThumbnailForLightboxImage( image, imageWidths.real, extraStatsDeferred );
+
+ this.resetBlurredThumbnailStates();
+ if ( imagePromise.state() === 'pending' ) {
+ this.displayPlaceholderThumbnail( image, $initialImage, imageWidths );
+ }
+
+ this.setupProgressBar( image, imagePromise, imageWidths.real );
+
+ metadataPromise = this.fetchSizeIndependentLightboxInfo( image.filePageTitle );
+
+ imagePromise.then(
+ // done
+ function ( thumbnail, imageElement ) {
+ if ( viewer.currentIndex !== image.index ) {
+ return;
+ }
+
+ if ( viewer.imageDisplayedCount++ === 0 ) {
+ mw.mmv.durationLogger.stop( 'click-to-first-image' );
+
+ metadataPromise.then( function ( imageInfo, repoInfo ) {
+ if ( imageInfo && imageInfo.anonymizedUploadDateTime ) {
+ mw.mmv.durationLogger.record( 'click-to-first-image', {
+ uploadTimestamp: imageInfo.anonymizedUploadDateTime
+ } );
+ }
+
+ return $.Deferred().resolve( imageInfo, repoInfo );
+ } );
+ }
+
+ imageElement.className = 'mw-mmv-final-image ' + image.filePageTitle.ext.toLowerCase();
+ imageElement.alt = image.alt;
+
+ $.when( metadataPromise, pluginsPromise ).then( function ( metadata ) {
+ $( document ).trigger( $.Event( 'mmv-metadata', { viewer: viewer, image: image, imageInfo: metadata[ 0 ] } ) );
+ } );
+
+ viewer.displayRealThumbnail( thumbnail, imageElement, imageWidths, $.now() - start );
+
+ return $.Deferred().resolve( thumbnail, imageElement );
+ },
+ // fail
+ function ( error ) {
+ viewer.ui.canvas.showError( error );
+ return $.Deferred().reject( error );
+ }
+ );
+
+ metadataPromise.then(
+ // done
+ function ( imageInfo, repoInfo ) {
+ extraStatsDeferred.resolve( { uploadTimestamp: imageInfo.anonymizedUploadDateTime } );
+
+ if ( viewer.currentIndex !== image.index ) {
+ return;
+ }
+
+ if ( viewer.metadataDisplayedCount++ === 0 ) {
+ mw.mmv.durationLogger.stop( 'click-to-first-metadata' ).record( 'click-to-first-metadata' );
+ }
+
+ viewer.ui.panel.setImageInfo( image, imageInfo, repoInfo );
+
+ // File reuse steals a bunch of information from the DOM, so do it last
+ viewer.ui.setFileReuseData( imageInfo, repoInfo, image.caption, image.alt );
+
+ return $.Deferred().resolve( imageInfo, repoInfo );
+ },
+ // fail
+ function ( error ) {
+ extraStatsDeferred.reject();
+
+ if ( viewer.currentIndex === image.index ) {
+ // Set title to caption or file name if caption is not available;
+ // see setTitle() in mmv.ui.metadataPanel for extended caption fallback
+ viewer.ui.panel.showError( image.caption || image.filePageTitle.getNameText(), error );
+ }
+
+ return $.Deferred().reject( error );
+ }
+ );
+
+ $.when( imagePromise, metadataPromise ).then( function () {
+ if ( viewer.currentIndex !== image.index ) {
+ return;
+ }
+
+ viewer.ui.panel.scroller.animateMetadataOnce();
+ viewer.preloadDependencies();
+ } );
+
+ this.comingFromHashChange = false;
+ };
+
+ /**
+ * Loads an image by its title
+ *
+ * @param {mw.Title} title
+ * @param {boolean} updateHash Viewer should update the location hash when true
+ */
+ MMVP.loadImageByTitle = function ( title, updateHash ) {
+ var viewer = this;
+
+ if ( !this.thumbs || !this.thumbs.length ) {
+ return;
+ }
+
+ this.comingFromHashChange = !updateHash;
+
+ $.each( this.thumbs, function ( idx, thumb ) {
+ if ( thumb.title.getPrefixedText() === title.getPrefixedText() ) {
+ viewer.loadImage( thumb.image, thumb.$thumb.clone()[ 0 ], true );
+ return false;
+ }
+ } );
+ };
+
+ /**
+ * Image loading progress. Keyed by image (database) name + '|' + thumbnail width in pixels,
+ * value is undefined, 'blurred' or 'real' (meaning respectively that no thumbnail is shown
+ * yet / the thumbnail that existed on the page is shown, enlarged and blurred / the real,
+ * correct-size thumbnail is shown).
+ *
+ * @private
+ * @property {Object.<string, string>}
+ */
+ MMVP.thumbnailStateCache = {};
+
+ /**
+ * Resets the cross-request states needed to handle the blurred thumbnail logic.
+ */
+ MMVP.resetBlurredThumbnailStates = function () {
+ /**
+ * Stores whether the real image was loaded and displayed already.
+ * This is reset when paging, so it is not necessarily accurate.
+ * @property {boolean}
+ */
+ this.realThumbnailShown = false;
+
+ /**
+ * Stores whether the a blurred placeholder is being displayed in place of the real image.
+ * When a placeholder is displayed, but it is not blurred, this is false.
+ * This is reset when paging, so it is not necessarily accurate.
+ * @property {boolean}
+ */
+ this.blurredThumbnailShown = false;
+ };
+
+ /**
+ * Display the real, full-resolution, thumbnail that was fetched with fetchThumbnail
+ *
+ * @param {mw.mmv.model.Thumbnail} thumbnail
+ * @param {HTMLImageElement} imageElement
+ * @param {mw.mmv.model.ThumbnailWidth} imageWidths
+ * @param {number} loadTime Time it took to load the thumbnail
+ */
+ MMVP.displayRealThumbnail = function ( thumbnail, imageElement, imageWidths, loadTime ) {
+ var viewer = this;
+
+ this.realThumbnailShown = true;
+
+ this.setImage( this.ui, thumbnail, imageElement, imageWidths );
+
+ // We only animate unblurWithAnimation if the image wasn't loaded from the cache
+ // A load in < 10ms is considered to be a browser cache hit
+ if ( this.blurredThumbnailShown && loadTime > 10 ) {
+ this.ui.canvas.unblurWithAnimation();
+ } else {
+ this.ui.canvas.unblur();
+ }
+
+ this.viewLogger.attach( thumbnail.url );
+
+ mw.mmv.actionLogger.log( 'image-view' ).then( function ( wasEventLogged ) {
+ viewer.viewLogger.setLastViewLogged( wasEventLogged );
+ } );
+ };
+
+ /**
+ * Display the blurred thumbnail from the page
+ *
+ * @param {mw.mmv.LightboxImage} image
+ * @param {jQuery} $initialImage The thumbnail from the page
+ * @param {mw.mmv.model.ThumbnailWidth} imageWidths
+ * @param {boolean} [recursion=false] for internal use, never set this when calling from outside
+ */
+ MMVP.displayPlaceholderThumbnail = function ( image, $initialImage, imageWidths, recursion ) {
+ var viewer = this,
+ size = { width: image.originalWidth, height: image.originalHeight };
+
+ // If the actual image has already been displayed, there's no point showing the blurry one.
+ // This can happen if the API request to get the original image size needed to show the
+ // placeholder thumbnail takes longer then loading the actual thumbnail.
+ if ( this.realThumbnailShown ) {
+ return;
+ }
+
+ // Width/height of the original image are added to the HTML by MediaViewer via a PHP hook,
+ // and can be missing in exotic circumstances, e. g. when the extension has only been
+ // enabled recently and the HTML cache has not cleared yet. If that is the case, we need
+ // to fetch the size from the API first.
+ if ( !size.width || !size.height ) {
+ if ( recursion ) {
+ // this should not be possible, but an infinite recursion is nasty
+ // business, so we make a sanity check
+ throw new Error( 'MediaViewer internal error: displayPlaceholderThumbnail recursion' );
+ }
+ this.imageInfoProvider.get( image.filePageTitle ).done( function ( imageInfo ) {
+ // Make sure the user has not navigated away while we were waiting for the size
+ if ( viewer.currentIndex === image.index ) {
+ image.originalWidth = imageInfo.width;
+ image.originalHeight = imageInfo.height;
+ viewer.displayPlaceholderThumbnail( image, $initialImage, imageWidths, true );
+ }
+ } );
+ } else {
+ this.blurredThumbnailShown = this.ui.canvas.maybeDisplayPlaceholder(
+ size, $initialImage, imageWidths );
+ }
+ };
+
+ /**
+ * Image loading progress. Keyed by image (database) name + '|' + thumbnail width in pixels,
+ * value is a number between 0-100.
+ *
+ * @private
+ * @property {Object.<string, number>}
+ */
+ MMVP.progressCache = {};
+
+ /**
+ * Displays a progress bar for the image loading, if necessary, and sets up handling of
+ * all the related callbacks.
+ *
+ * @param {mw.mmv.LightboxImage} image
+ * @param {jQuery.Promise.<mw.mmv.model.Thumbnail, HTMLImageElement>} imagePromise
+ * @param {number} imageWidth needed for caching progress (FIXME)
+ */
+ MMVP.setupProgressBar = function ( image, imagePromise, imageWidth ) {
+ var viewer = this,
+ progressBar = viewer.ui.panel.progressBar,
+ key = image.filePageTitle.getPrefixedDb() + '|' + imageWidth;
+
+ if ( !this.progressCache[ key ] ) {
+ // Animate progress bar to 5 to give a sense that something is happening, and make sure
+ // the progress bar is noticeable, even if we're sitting at 0% stuck waiting for
+ // server-side processing, such as thumbnail (re)generation
+ progressBar.jumpTo( 0 );
+ progressBar.animateTo( 5 );
+ viewer.progressCache[ key ] = 5;
+ } else {
+ progressBar.jumpTo( this.progressCache[ key ] );
+ }
+
+ // FIXME would be nice to have a "filtered" promise which does not fire when the image is not visible
+ imagePromise.then(
+ // done
+ function ( thumbnail, imageElement ) {
+ viewer.progressCache[ key ] = 100;
+ if ( viewer.currentIndex === image.index ) {
+ // Fallback in case the browser doesn't have fancy progress updates
+ progressBar.animateTo( 100 );
+
+ // Hide progress bar, we're done
+ progressBar.hide();
+ }
+
+ return $.Deferred().resolve( thumbnail, imageElement );
+ },
+ // fail
+ function ( error ) {
+ viewer.progressCache[ key ] = 100;
+
+ if ( viewer.currentIndex === image.index ) {
+ // Hide progress bar on error
+ progressBar.hide();
+ }
+
+ return $.Deferred().reject( error );
+ },
+ // progress
+ function ( progress ) {
+ // We pretend progress is always at least 5%, so progress events below 5% should be ignored
+ // 100 will be handled by the done handler, do not mix two animations
+ if ( progress >= 5 && progress < 100 ) {
+ viewer.progressCache[ key ] = progress;
+
+ // Touch the UI only if the user is looking at this image
+ if ( viewer.currentIndex === image.index ) {
+ progressBar.animateTo( progress );
+ }
+ }
+
+ return progress;
+ }
+ );
+ };
+
+ /**
+ * Preload this many prev/next images to speed up navigation.
+ * (E.g. preloadDistance = 3 means that the previous 3 and the next 3 images will be loaded.)
+ * Preloading only happens when the viewer is open.
+ * @property {number}
+ */
+ MMVP.preloadDistance = 1;
+
+ /**
+ * Stores image metadata preloads, so they can be cancelled.
+ * @property {mw.mmv.model.TaskQueue}
+ */
+ MMVP.metadataPreloadQueue = null;
+
+ /**
+ * Stores image thumbnail preloads, so they can be cancelled.
+ * @property {mw.mmv.model.TaskQueue}
+ */
+ MMVP.thumbnailPreloadQueue = null;
+
+ /**
+ * Orders lightboximage indexes for preloading. Works similar to $.each, except it only takes
+ * the callback argument. Calls the callback with each lightboximage index in some sequence
+ * that is ideal for preloading.
+ *
+ * @private
+ * @param {function(number, mw.mmv.LightboxImage)} callback
+ */
+ MMVP.eachPrealoadableLightboxIndex = function ( callback ) {
+ var i;
+ for ( i = 0; i <= this.preloadDistance; i++ ) {
+ if ( this.currentIndex + i < this.thumbs.length ) {
+ callback(
+ this.currentIndex + i,
+ this.thumbs[ this.currentIndex + i ].image,
+ this.thumbs[ this.currentIndex + i ].extraStatsDeferred
+ );
+ }
+ if ( i && this.currentIndex - i >= 0 ) { // skip duplicate for i==0
+ callback(
+ this.currentIndex - i,
+ this.thumbs[ this.currentIndex - i ].image,
+ this.thumbs[ this.currentIndex - i ].extraStatsDeferred
+ );
+ }
+ }
+ };
+
+ /**
+ * A helper function to fill up the preload queues.
+ * taskFactory(lightboxImage) should return a preload task for the given lightboximage.
+ *
+ * @private
+ * @param {function(mw.mmv.LightboxImage): function()} taskFactory
+ * @return {mw.mmv.model.TaskQueue}
+ */
+ MMVP.pushLightboxImagesIntoQueue = function ( taskFactory ) {
+ var queue = new mw.mmv.model.TaskQueue();
+
+ this.eachPrealoadableLightboxIndex( function ( i, lightboxImage, extraStatsDeferred ) {
+ queue.push( taskFactory( lightboxImage, extraStatsDeferred ) );
+ } );
+
+ return queue;
+ };
+
+ /**
+ * Cancels in-progress image metadata preloading.
+ */
+ MMVP.cancelImageMetadataPreloading = function () {
+ if ( this.metadataPreloadQueue ) {
+ this.metadataPreloadQueue.cancel();
+ }
+ };
+
+ /**
+ * Cancels in-progress image thumbnail preloading.
+ */
+ MMVP.cancelThumbnailsPreloading = function () {
+ if ( this.thumbnailPreloadQueue ) {
+ this.thumbnailPreloadQueue.cancel();
+ }
+ };
+
+ /**
+ * Preload metadata for next and prev N image (N = MMVP.preloadDistance).
+ * Two images will be loaded at a time (one forward, one backward), with closer images
+ * being loaded sooner.
+ */
+ MMVP.preloadImagesMetadata = function () {
+ var viewer = this;
+
+ this.cancelImageMetadataPreloading();
+
+ this.metadataPreloadQueue = this.pushLightboxImagesIntoQueue( function ( lightboxImage, extraStatsDeferred ) {
+ return function () {
+ var metadatapromise = viewer.fetchSizeIndependentLightboxInfo( lightboxImage.filePageTitle );
+ metadatapromise.done( function ( imageInfo ) {
+ extraStatsDeferred.resolve( { uploadTimestamp: imageInfo.anonymizedUploadDateTime } );
+ } ).fail( function () {
+ extraStatsDeferred.reject();
+ } );
+ return metadatapromise;
+ };
+ } );
+
+ this.metadataPreloadQueue.execute();
+ };
+
+ /**
+ * Preload thumbnails for next and prev N image (N = MMVP.preloadDistance).
+ * Two images will be loaded at a time (one forward, one backward), with closer images
+ * being loaded sooner.
+ */
+ MMVP.preloadThumbnails = function () {
+ var viewer = this;
+
+ this.cancelThumbnailsPreloading();
+
+ this.thumbnailPreloadQueue = this.pushLightboxImagesIntoQueue( function ( lightboxImage, extraStatsDeferred ) {
+ return function () {
+ var imageWidths, canvasDimensions;
+
+ // viewer.ui.canvas.getLightboxImageWidths needs the viewer to be open
+ // because it needs to read the size of visible elements
+ if ( !viewer.isOpen ) {
+ return;
+ }
+
+ imageWidths = viewer.ui.canvas.getLightboxImageWidths( lightboxImage );
+ canvasDimensions = viewer.ui.canvas.getDimensions();
+
+ mw.mmv.dimensionLogger.logDimensions( imageWidths, canvasDimensions, 'preload' );
+
+ return viewer.fetchThumbnailForLightboxImage( lightboxImage, imageWidths.real, extraStatsDeferred );
+ };
+ } );
+
+ this.thumbnailPreloadQueue.execute();
+ };
+
+ /**
+ * Preload the fullscreen size of the current image.
+ *
+ * @param {mw.mmv.LightboxImage} image
+ */
+ MMVP.preloadFullscreenThumbnail = function ( image ) {
+ var imageWidths = this.ui.canvas.getLightboxImageWidthsForFullscreen( image ),
+ canvasDimensions = this.ui.canvas.getDimensions( true );
+
+ mw.mmv.dimensionLogger.logDimensions( imageWidths, canvasDimensions, 'preload' );
+ this.fetchThumbnailForLightboxImage( image, imageWidths.real );
+ };
+
+ /**
+ * Loads all the size-independent information needed by the lightbox (image metadata, repo
+ * information).
+ *
+ * @param {mw.Title} fileTitle Title of the file page for the image.
+ * @return {jQuery.Promise.<mw.mmv.model.Image, mw.mmv.model.Repo>}
+ */
+ MMVP.fetchSizeIndependentLightboxInfo = function ( fileTitle ) {
+ var imageInfoPromise = this.imageInfoProvider.get( fileTitle ),
+ repoInfoPromise = this.fileRepoInfoProvider.get( fileTitle );
+
+ return $.when(
+ imageInfoPromise, repoInfoPromise
+ ).then( function ( imageInfo, repoInfoHash ) {
+ return $.Deferred().resolve( imageInfo, repoInfoHash[ imageInfo.repo ] );
+ } );
+ };
+
+ /**
+ * Loads size-dependent components of a lightbox - the thumbnail model and the image itself.
+ *
+ * @param {mw.mmv.LightboxImage} image
+ * @param {number} width the width of the requested thumbnail
+ * @param {jQuery.Deferred.<string>} [extraStatsDeferred] Promise that resolves to the image's upload timestamp when the metadata is loaded
+ * @return {jQuery.Promise.<mw.mmv.model.Thumbnail, HTMLImageElement>}
+ */
+ MMVP.fetchThumbnailForLightboxImage = function ( image, width, extraStatsDeferred ) {
+ return this.fetchThumbnail(
+ image.filePageTitle,
+ width,
+ image.src,
+ image.originalWidth,
+ image.originalHeight,
+ extraStatsDeferred
+ );
+ };
+
+ /**
+ * Loads size-dependent components of a lightbox - the thumbnail model and the image itself.
+ *
+ * @param {mw.Title} fileTitle
+ * @param {number} width the width of the requested thumbnail
+ * @param {string} [sampleUrl] a thumbnail URL for the same file (but with different size) (might be missing)
+ * @param {number} [originalWidth] the width of the original, full-sized file (might be missing)
+ * @param {number} [originalHeight] the height of the original, full-sized file (might be missing)
+ * @param {jQuery.Deferred.<string>} [extraStatsDeferred] Promise that resolves to the image's upload timestamp when the metadata is loaded
+ * @return {jQuery.Promise.<mw.mmv.model.Thumbnail, HTMLImageElement>} A promise resolving to
+ * a thumbnail model and an <img> element. It might or might not have progress events which
+ * return a single number.
+ */
+ MMVP.fetchThumbnail = function ( fileTitle, width, sampleUrl, originalWidth, originalHeight, extraStatsDeferred ) {
+ var viewer = this,
+ guessing = false,
+ combinedDeferred = $.Deferred(),
+ thumbnailPromise,
+ imagePromise;
+
+ if ( originalWidth && width > originalWidth ) {
+ // Do not request images larger than the original image
+ // This would be possible (but still unwanted) for SVG images
+ width = originalWidth;
+ }
+
+ if (
+ sampleUrl && originalWidth && originalHeight &&
+ this.config.useThumbnailGuessing()
+ ) {
+ guessing = true;
+ thumbnailPromise = this.guessedThumbnailInfoProvider.get(
+ fileTitle, sampleUrl, width, originalWidth, originalHeight
+ ).then( null, function () { // catch rejection, use fallback
+ return viewer.thumbnailInfoProvider.get( fileTitle, width );
+ } );
+ } else {
+ thumbnailPromise = this.thumbnailInfoProvider.get( fileTitle, width );
+ }
+
+ // Add thumbnail width to the extra stats passed to the performance log
+ extraStatsDeferred = $.when( extraStatsDeferred || {} ).then( function ( extraStats ) {
+ extraStats.imageWidth = width;
+ return extraStats;
+ } );
+
+ imagePromise = thumbnailPromise.then( function ( thumbnail ) {
+ return viewer.imageProvider.get( thumbnail.url, extraStatsDeferred );
+ } );
+
+ if ( guessing ) {
+ // If we guessed wrong, need to retry with real URL on failure.
+ // As a side effect this introduces an extra (harmless) retry of a failed thumbnailInfoProvider.get call
+ // because thumbnailInfoProvider.get is already called above when guessedThumbnailInfoProvider.get fails.
+ imagePromise = imagePromise.then( null, function () {
+ return viewer.thumbnailInfoProvider.get( fileTitle, width ).then( function ( thumbnail ) {
+ return viewer.imageProvider.get( thumbnail.url, extraStatsDeferred );
+ } );
+ } );
+ }
+
+ // In jQuery<3, $.when used to also relay notify, but that is no longer
+ // the case - but we still want to pass it along...
+ $.when( thumbnailPromise, imagePromise ).then( combinedDeferred.resolve, combinedDeferred.reject );
+ imagePromise.then( null, null, function ( arg, progress ) {
+ combinedDeferred.notify( progress );
+ } );
+ return combinedDeferred;
+ };
+
+ /**
+ * Loads an image at a specified index in the viewer's thumbnail array.
+ *
+ * @param {number} index
+ */
+ MMVP.loadIndex = function ( index ) {
+ var thumb;
+
+ if ( index < this.thumbs.length && index >= 0 ) {
+ this.viewLogger.recordViewDuration();
+
+ thumb = this.thumbs[ index ];
+ this.loadImage( thumb.image, thumb.$thumb.clone()[ 0 ] );
+ }
+ };
+
+ /**
+ * Opens the next image
+ */
+ MMVP.nextImage = function () {
+ mw.mmv.actionLogger.log( 'next-image' );
+ this.loadIndex( this.currentIndex + 1 );
+ };
+
+ /**
+ * Opens the previous image
+ */
+ MMVP.prevImage = function () {
+ mw.mmv.actionLogger.log( 'prev-image' );
+ this.loadIndex( this.currentIndex - 1 );
+ };
+
+ /**
+ * Handles close event coming from the lightbox
+ */
+ MMVP.close = function () {
+ var windowTitle;
+
+ this.viewLogger.recordViewDuration();
+ this.viewLogger.unattach();
+
+ windowTitle = this.createDocumentTitle( null );
+
+ if ( comingFromHashChange === false ) {
+ $( document ).trigger( $.Event( 'mmv-hash', { hash: '#', title: windowTitle } ) );
+ } else {
+ comingFromHashChange = false;
+ }
+
+ // This has to happen after the hash reset, because setting the hash to # will reset the page scroll
+ $( document ).trigger( $.Event( 'mmv-cleanup-overlay' ) );
+
+ this.isOpen = false;
+ };
+
+ /**
+ * Handles a hash change coming from the browser
+ */
+ MMVP.hash = function () {
+ var route = this.router.parseLocation( window.location );
+
+ if ( route instanceof mw.mmv.routing.ThumbnailRoute ) {
+ document.title = this.createDocumentTitle( route.fileTitle );
+ this.loadImageByTitle( route.fileTitle );
+ } else if ( this.isOpen ) {
+ // This allows us to avoid the mmv-hash event that normally happens on close
+ comingFromHashChange = true;
+
+ document.title = this.createDocumentTitle( null );
+ if ( this.ui ) {
+ // FIXME triggers mmv-close event, which calls viewer.close()
+ this.ui.unattach();
+ } else {
+ this.close();
+ }
+ }
+ };
+
+ MMVP.setHash = function () {
+ var route, windowTitle, hashFragment;
+ if ( !this.comingFromHashChange ) {
+ route = new mw.mmv.routing.ThumbnailRoute( this.currentImageFileTitle );
+ hashFragment = '#' + this.router.createHash( route );
+ windowTitle = this.createDocumentTitle( this.currentImageFileTitle );
+ $( document ).trigger( $.Event( 'mmv-hash', { hash: hashFragment, title: windowTitle } ) );
+ }
+ };
+
+ /**
+ * Creates a string which can be shown as document title (the text at the top of the browser window).
+ *
+ * @param {mw.Title|null} imageTitle the title object for the image which is displayed; null when the
+ * viewer is being closed
+ * @return {string}
+ */
+ MMVP.createDocumentTitle = function ( imageTitle ) {
+ if ( imageTitle ) {
+ return imageTitle.getNameText() + ' - ' + this.documentTitle;
+ } else {
+ return this.documentTitle;
+ }
+ };
+
+ /**
+ * @event mmv-close
+ * Fired when the viewer is closed. This is used by the ligthbox to notify the main app.
+ */
+ /**
+ * @event mmv-next
+ * Fired when the user requests the next image.
+ */
+ /**
+ * @event mmv-prev
+ * Fired when the user requests the previous image.
+ */
+ /**
+ * @event mmv-resize-end
+ * Fired when the screen size changes. Debounced to avoid continous triggering while resizing with a mouse.
+ */
+ /**
+ * @event mmv-request-thumbnail
+ * Used by components to request a thumbnail URL for the current thumbnail, with a given size.
+ * @param {number} size
+ */
+ /**
+ * Registers all event handlers
+ */
+ MMVP.setupEventHandlers = function () {
+ var viewer = this;
+
+ this.ui.connect( this, {
+ next: 'nextImage',
+ prev: 'prevImage'
+ } );
+
+ $( document ).on( 'mmv-close.mmvp', function () {
+ viewer.close();
+ } ).on( 'mmv-resize-end.mmvp', function () {
+ viewer.resize( viewer.ui );
+ } ).on( 'mmv-request-thumbnail.mmvp', function ( e, size ) {
+ if ( viewer.currentImageFileTitle ) {
+ return viewer.thumbnailInfoProvider.get( viewer.currentImageFileTitle, size );
+ } else {
+ return $.Deferred().reject();
+ }
+ } ).on( 'mmv-viewfile.mmvp', function () {
+ viewer.imageInfoProvider.get( viewer.currentImageFileTitle ).done( function ( imageInfo ) {
+ document.location = imageInfo.url;
+ } );
+ } );
+ };
+
+ /**
+ * Unregisters all event handlers. Currently only used in tests.
+ */
+ MMVP.cleanupEventHandlers = function () {
+ $( document ).off( 'mmv-close.mmvp mmv-resize-end.mmvp' );
+
+ this.ui.disconnect( this );
+ };
+
+ /**
+ * Preloads JS and CSS dependencies that aren't needed to display the first image, but could be needed later
+ */
+ MMVP.preloadDependencies = function () {
+ mw.loader.load( [ 'mmv.ui.reuse.shareembed', 'moment' ] );
+ };
+
+ /**
+ * Loads the RL module defined for a given file extension, if any
+ *
+ * @param {string} extension File extension
+ * @return {jQuery.Promise}
+ */
+ MMVP.loadExtensionPlugins = function ( extension ) {
+ var deferred = $.Deferred(),
+ config = this.config.extensions();
+
+ if ( !( extension in config ) || config[ extension ] === 'default' ) {
+ return deferred.resolve();
+ }
+
+ mw.loader.using( config[ extension ], function () {
+ deferred.resolve();
+ } );
+
+ return deferred;
+ };
+
+ mw.mmv.MultimediaViewer = MultimediaViewer;
+}( mediaWiki, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.lightboximage.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.lightboximage.js
new file mode 100644
index 00000000..865666d5
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.lightboximage.js
@@ -0,0 +1,63 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw ) {
+
+ /**
+ * Represents an image on the page.
+ *
+ * @class mw.mmv.LightboxImage
+ * @constructor
+ * @param {string} fileLink Link to the file - generally a thumb URL
+ * @param {string} filePageLink Link to the File: page
+ * @param {mw.Title} fileTitle Represents the File: page
+ * @param {number} index Which number file this is
+ * @param {HTMLImageElement} thumb The thumbnail that represents this image on the page
+ * @param {string} [caption] The caption, if any.
+ * @param {string} [alt] The alt text of the image
+ */
+ function LightboxImage( fileLink, filePageLink, fileTitle, index, thumb, caption, alt ) {
+ /** @property {string} Link to the file - generally a thumb URL */
+ this.src = fileLink;
+
+ /** @property {string} filePageLink URL to the image's file page */
+ this.filePageLink = filePageLink;
+
+ /** @property {mw.Title} filePageTitle Title of the image's file page */
+ this.filePageTitle = fileTitle;
+
+ /** @property {number} index What number this image is in the array of indexed images */
+ this.index = index;
+
+ /** @property {HTMLImageElement} thumbnail The <img> element that holds the already-loaded thumbnail of the image*/
+ this.thumbnail = thumb;
+
+ /** @property {string} caption The caption of the image, if any */
+ this.caption = caption;
+
+ /** @property {string} alt The alt text of the image */
+ this.alt = alt;
+
+ /** @property {number|undefined} originalWidth Width of the full-sized file (read from HTML data attribute, might be missing) */
+ this.originalWidth = undefined;
+
+ /** @property {number|undefined} originalHeight Height of the full-sized file (read from HTML data attribute, might be missing) */
+ this.originalHeight = undefined;
+ }
+
+ mw.mmv.LightboxImage = LightboxImage;
+}( mediaWiki, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.lightboxinterface.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.lightboxinterface.js
new file mode 100644
index 00000000..21f33a06
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.lightboxinterface.js
@@ -0,0 +1,509 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $, oo ) {
+ var LIP;
+
+ /**
+ * Represents the main interface of the lightbox
+ *
+ * @class mw.mmv.LightboxInterface
+ * @extends mw.mmv.ui.Element
+ * @constructor
+ */
+ function LightboxInterface() {
+ this.localStorage = mw.storage;
+
+ /** @property {mw.mmv.Config} config - */
+ this.config = new mw.mmv.Config(
+ mw.config.get( 'wgMultimediaViewer', {} ),
+ mw.config,
+ mw.user,
+ new mw.Api(),
+ this.localStorage
+ );
+
+ /**
+ * @property {mw.mmv.ThumbnailWidthCalculator}
+ * @private
+ */
+ this.thumbnailWidthCalculator = new mw.mmv.ThumbnailWidthCalculator();
+
+ this.init();
+ mw.mmv.ui.Element.call( this, this.$wrapper );
+ }
+ oo.inheritClass( LightboxInterface, mw.mmv.ui.Element );
+ LIP = LightboxInterface.prototype;
+
+ /**
+ * The currently selected LightboxImage.
+ *
+ * @type {mw.mmv.LightboxImage}
+ * @protected
+ */
+ LIP.currentImage = null;
+
+ /**
+ * Initialize the entire interface - helper method.
+ */
+ LIP.init = function () {
+ // SVG filter, needed to achieve blur in Firefox
+ this.$filter = $( '<svg><filter id="gaussian-blur"><fegaussianblur stdDeviation="3"></filter></svg>' );
+
+ this.$wrapper = $( '<div>' )
+ .addClass( 'mw-mmv-wrapper' );
+
+ this.$main = $( '<div>' )
+ .addClass( 'mw-mmv-main' );
+
+ // I blame CSS for this
+ this.$innerWrapper = $( '<div>' )
+ .addClass( 'mw-mmv-image-inner-wrapper' );
+
+ this.$imageWrapper = $( '<div>' )
+ .addClass( 'mw-mmv-image-wrapper' )
+ .append( this.$innerWrapper );
+
+ this.$preDiv = $( '<div>' )
+ .addClass( 'mw-mmv-pre-image' );
+
+ this.$postDiv = $( '<div>' )
+ .addClass( 'mw-mmv-post-image' );
+
+ this.$aboveFold = $( '<div>' )
+ .addClass( 'mw-mmv-above-fold' );
+
+ this.$main.append(
+ this.$preDiv,
+ this.$imageWrapper,
+ this.$postDiv,
+ this.$filter
+ );
+
+ this.$wrapper.append(
+ this.$main
+ );
+
+ this.setupCanvasButtons();
+
+ this.panel = new mw.mmv.ui.MetadataPanel( this.$postDiv, this.$aboveFold, this.localStorage, this.config );
+ this.buttons = new mw.mmv.ui.CanvasButtons( this.$preDiv, this.$closeButton, this.$fullscreenButton );
+ this.canvas = new mw.mmv.ui.Canvas( this.$innerWrapper, this.$imageWrapper, this.$wrapper );
+
+ this.fileReuse = new mw.mmv.ui.reuse.Dialog( this.$innerWrapper, this.buttons.$reuse, this.config );
+ this.downloadDialog = new mw.mmv.ui.download.Dialog( this.$innerWrapper, this.buttons.$download, this.config );
+ this.optionsDialog = new mw.mmv.ui.OptionsDialog( this.$innerWrapper, this.buttons.$options, this.config );
+ };
+
+ /**
+ * Sets up the file reuse data in the DOM
+ *
+ * @param {mw.mmv.model.Image} image
+ * @param {mw.mmv.model.Repo} repo
+ * @param {string} caption
+ * @param {string} alt
+ */
+ LIP.setFileReuseData = function ( image, repo, caption, alt ) {
+ this.fileReuse.set( image, repo, caption, alt );
+ this.downloadDialog.set( image, repo );
+ };
+
+ /**
+ * Empties the interface.
+ */
+ LIP.empty = function () {
+ this.panel.empty();
+
+ this.canvas.empty();
+
+ this.buttons.empty();
+
+ this.$main.addClass( 'metadata-panel-is-closed' )
+ .removeClass( 'metadata-panel-is-open' );
+ };
+
+ /**
+ * Opens the lightbox.
+ */
+ LIP.open = function () {
+ this.empty();
+ this.attach();
+ };
+
+ /**
+ * Attaches the interface to the DOM.
+ *
+ * @param {string} [parentId] parent id where we want to attach the UI. Defaults to document
+ * element, override is mainly used for testing.
+ */
+ LIP.attach = function ( parentId ) {
+ var ui = this,
+ $parent;
+
+ // Advanced description needs to be below the fold when the lightbox opens
+ // regardless of what the scroll value was prior to opening the lightbox
+ // If the lightbox is already attached, it means we're doing prev/next, and
+ // we should avoid scrolling the panel
+ if ( !this.attached ) {
+ $( window ).scrollTop( 0 );
+ }
+
+ // Make sure that the metadata is going to be at the bottom when it appears
+ // 83 is the height of the top metadata area. Which can't be measured by
+ // reading the DOM at this point of the execution, unfortunately
+ this.$postDiv.css( 'top', ( $( window ).height() - 83 ) + 'px' );
+
+ // Re-appending the same content can have nasty side-effects
+ // Such as the browser leaving fullscreen mode if the fullscreened element is part of it
+ if ( this.currentlyAttached ) {
+ return;
+ }
+
+ this.handleEvent( 'keyup', function ( e ) {
+ if ( e.keyCode === 27 && !( e.altKey || e.ctrlKey || e.shiftKey || e.metaKey ) ) {
+ // Escape button pressed
+ ui.unattach();
+ }
+ } );
+
+ this.handleEvent( 'jq-fullscreen-change.lip', function ( e ) {
+ ui.fullscreenChange( e );
+ } );
+
+ this.handleEvent( 'keydown', function ( e ) { ui.keydown( e ); } );
+
+ // mousemove generates a ton of events, which is why we throttle it
+ this.handleEvent( 'mousemove.lip', $.throttle( 250, function ( e ) {
+ ui.mousemove( e );
+ } ) );
+
+ this.handleEvent( 'mmv-faded-out', function ( e ) { ui.fadedOut( e ); } );
+ this.handleEvent( 'mmv-fade-stopped', function ( e ) { ui.fadeStopped( e ); } );
+
+ this.buttons.connect( this, {
+ next: [ 'emit', 'next' ],
+ prev: [ 'emit', 'prev' ]
+ } );
+
+ $parent = $( parentId || document.body );
+
+ // Clean up fullscreen data because hard-existing fullscreen might have left
+ // jquery.fullscreen unable to remove the class and attribute, since $main wasn't
+ // attached to the DOM anymore at the time the jq-fullscreen-change event triggered
+ this.$main.data( 'isFullscreened', false ).removeClass( 'jq-fullscreened' );
+ this.isFullscreen = false;
+
+ $parent
+ .append(
+ this.$wrapper
+ );
+ this.currentlyAttached = true;
+
+ this.panel.attach();
+
+ this.canvas.attach();
+
+ // cross-communication between panel and canvas, sort of
+ this.$postDiv.on( 'mmv-metadata-open.lip', function () {
+ ui.$main.addClass( 'metadata-panel-is-open' )
+ .removeClass( 'metadata-panel-is-closed' );
+ } ).on( 'mmv-metadata-close.lip', function () {
+ ui.$main.removeClass( 'metadata-panel-is-open' )
+ .addClass( 'metadata-panel-is-closed' );
+ } );
+ this.$wrapper.on( 'mmv-panel-close-area-click.lip', function () {
+ ui.panel.scroller.toggle( 'down' );
+ } );
+
+ // Buttons fading might not had been reset properly after a hard fullscreen exit
+ // This needs to happen after the parent attach() because the buttons need to be attached
+ // to the DOM for $.fn.stop() to work
+ this.buttons.stopFade();
+ this.buttons.attach();
+
+ this.fileReuse.attach();
+ this.downloadDialog.attach();
+ this.optionsDialog.attach();
+
+ // Reset the cursor fading
+ this.fadeStopped();
+
+ this.attached = true;
+ };
+
+ /**
+ * Detaches the interface from the DOM.
+ */
+ LIP.unattach = function () {
+ mw.mmv.actionLogger.log( 'close' );
+
+ // Has to happen first so that the scroller can freeze with visible elements
+ this.panel.unattach();
+
+ this.$wrapper.detach();
+
+ this.currentlyAttached = false;
+
+ this.buttons.unattach();
+
+ this.$postDiv.off( '.lip' );
+ this.$wrapper.off( 'mmv-panel-close-area-click.lip' );
+
+ this.fileReuse.unattach();
+ this.fileReuse.closeDialog();
+
+ this.downloadDialog.unattach();
+ this.downloadDialog.closeDialog();
+
+ this.optionsDialog.unattach();
+ this.optionsDialog.closeDialog();
+
+ // Canvas listens for events from dialogs, so should be unattached at the end
+ this.canvas.unattach();
+
+ this.clearEvents();
+
+ this.buttons.disconnect( this, {
+ next: [ 'emit', 'next' ],
+ prev: [ 'emit', 'prev' ]
+ } );
+
+ // We trigger this event on the document because unattach() can run
+ // when the interface is unattached
+ $( document ).trigger( $.Event( 'mmv-close' ) )
+ .off( 'jq-fullscreen-change.lip' );
+
+ this.attached = false;
+ };
+
+ /**
+ * Exits fullscreen mode.
+ */
+ LIP.exitFullscreen = function () {
+ this.fullscreenButtonJustPressed = true;
+ this.$main.exitFullscreen();
+ };
+
+ /**
+ * Enters fullscreen mode.
+ */
+ LIP.enterFullscreen = function () {
+ this.$main.enterFullscreen();
+ };
+
+ /**
+ * Setup for canvas navigation buttons
+ */
+ LIP.setupCanvasButtons = function () {
+ var ui = this,
+ tooltipDelay = mw.config.get( 'wgMultimediaViewer' ).tooltipDelay;
+
+ this.$closeButton = $( '<button>' )
+ .text( ' ' )
+ .addClass( 'mw-mmv-close' )
+ .prop( 'title', mw.message( 'multimediaviewer-close-popup-text' ).text() )
+ .tipsy( {
+ delayIn: tooltipDelay,
+ gravity: this.correctEW( 'ne' )
+ } )
+ .click( function () {
+ if ( ui.isFullscreen ) {
+ ui.$main.trigger( $.Event( 'jq-fullscreen-change.lip' ) );
+ }
+ ui.unattach();
+ } );
+
+ this.$fullscreenButton = $( '<button>' )
+ .text( ' ' )
+ .addClass( 'mw-mmv-fullscreen' )
+ .prop( 'title', mw.message( 'multimediaviewer-fullscreen-popup-text' ).text() )
+ .tipsy( {
+ delayIn: tooltipDelay,
+ gravity: this.correctEW( 'ne' )
+ } )
+ .click( function ( e ) {
+ if ( ui.isFullscreen ) {
+ ui.exitFullscreen();
+
+ // mousemove is throttled and the mouse coordinates only
+ // register every 250ms, so there is a chance that we moved
+ // our mouse over one of the buttons but it didn't register,
+ // and a fadeOut is triggered; when we're coming back from
+ // fullscreen, we'll want to make sure the mouse data is
+ // current so that the fadeOut behavior will not trigger
+ ui.mousePosition = { x: e.pageX, y: e.pageY };
+ ui.buttons.revealAndFade( ui.mousePosition );
+ } else {
+ ui.enterFullscreen();
+ }
+ } );
+
+ // If the browser doesn't support fullscreen mode, hide the fullscreen button
+ if ( $.support.fullscreen ) {
+ this.$fullscreenButton.show();
+ } else {
+ this.$fullscreenButton.hide();
+ }
+ };
+
+ /**
+ * Handle a fullscreen change event.
+ *
+ * @param {jQuery.Event} e The fullscreen change event.
+ */
+ LIP.fullscreenChange = function ( e ) {
+ this.isFullscreen = e.fullscreen;
+
+ if ( this.isFullscreen ) {
+ mw.mmv.actionLogger.log( 'fullscreen' );
+
+ this.$fullscreenButton
+ .prop( 'title', mw.message( 'multimediaviewer-defullscreen-popup-text' ).text() )
+ .attr( 'alt', mw.message( 'multimediaviewer-defullscreen-popup-text' ).text() );
+ } else {
+ mw.mmv.actionLogger.log( 'defullscreen' );
+
+ this.$fullscreenButton
+ .prop( 'title', mw.message( 'multimediaviewer-fullscreen-popup-text' ).text() )
+ .attr( 'alt', mw.message( 'multimediaviewer-fullscreen-popup-text' ).text() );
+ }
+
+ if ( !this.fullscreenButtonJustPressed && !e.fullscreen ) {
+ // Close the interface all the way if the user pressed 'esc'
+ this.unattach();
+ } else if ( this.fullscreenButtonJustPressed ) {
+ this.fullscreenButtonJustPressed = false;
+ }
+
+ // Fullscreen change events can happen after unattach(), in which
+ // case we shouldn't do anything UI-related
+ if ( !this.currentlyAttached ) {
+ return;
+ }
+
+ if ( this.isFullscreen ) {
+ // When entering fullscreen without a mousemove, the browser
+ // still thinks that the cursor is where it was prior to entering
+ // fullscreen. I.e. on top of the fullscreen button
+ // Thus, we purposefully reset the saved position, so that
+ // the fade out really takes place (otherwise it's cancelled
+ // by updateControls which is called a few times when fullscreen opens)
+ this.mousePosition = { x: 0, y: 0 };
+ this.buttons.fadeOut();
+ }
+
+ // Some browsers only send resize events before toggling fullscreen, but not once the toggling is done
+ // This makes sure that the UI is properly resized after a fullscreen change
+ this.$main.trigger( $.Event( 'mmv-resize-end' ) );
+ };
+
+ /**
+ * Handles keydown events on the document
+ *
+ * @param {jQuery.Event} e The jQuery keypress event object
+ */
+ LIP.keydown = function ( e ) {
+ var forward,
+ isRtl = $( document.body ).hasClass( 'rtl' );
+
+ if ( e.altKey || e.shiftKey || e.ctrlKey || e.metaKey ) {
+ return;
+ }
+
+ switch ( e.which ) {
+ case 37: // Left arrow
+ case 39: // Right arrow
+ e.preventDefault();
+ forward = ( e.which === 39 );
+ if ( isRtl ) {
+ forward = !forward;
+ }
+
+ if ( forward ) {
+ this.emit( 'next' );
+ } else {
+ this.emit( 'prev' );
+ }
+
+ e.preventDefault();
+ break;
+ }
+ };
+
+ /**
+ * Handles mousemove events on the document
+ *
+ * @param {jQuery.Event} e The mousemove event object
+ */
+ LIP.mousemove = function ( e ) {
+ // T77869 ignore fake mousemove events triggered by Chrome
+ if (
+ e &&
+ e.originalEvent &&
+ e.originalEvent.movementX === 0 &&
+ e.originalEvent.movementY === 0
+ ) {
+ return;
+ }
+
+ if ( e ) {
+ // Saving the mouse position is useful whenever we need to
+ // run LIP.mousemove manually, such as when going to the next/prev
+ // element
+ this.mousePosition = { x: e.pageX, y: e.pageY };
+ }
+
+ if ( this.isFullscreen ) {
+ this.buttons.revealAndFade( this.mousePosition );
+ }
+ };
+
+ /**
+ * Called when the buttons have completely faded out and disappeared
+ */
+ LIP.fadedOut = function () {
+ this.$main.addClass( 'cursor-hidden' );
+ };
+
+ /**
+ * Called when the buttons have stopped fading and are back into view
+ */
+ LIP.fadeStopped = function () {
+ this.$main.removeClass( 'cursor-hidden' );
+ };
+
+ /**
+ * Updates the next and prev buttons
+ *
+ * @param {boolean} showPrevButton Whether the prev button should be revealed or not
+ * @param {boolean} showNextButton Whether the next button should be revealed or not
+ */
+ LIP.updateControls = function ( showPrevButton, showNextButton ) {
+ var prevNextTop = ( ( this.$imageWrapper.height() / 2 ) - 60 ) + 'px';
+
+ if ( this.$main.data( 'isFullscreened' ) ) {
+ this.$postDiv.css( 'top', '' );
+ } else {
+ this.$postDiv.css( 'top', this.$imageWrapper.height() );
+ }
+
+ this.buttons.setOffset( prevNextTop );
+ this.buttons.toggle( showPrevButton, showNextButton );
+ };
+
+ mw.mmv.LightboxInterface = LightboxInterface;
+}( mediaWiki, jQuery, OO ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.lightboxinterface.less b/www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.lightboxinterface.less
new file mode 100644
index 00000000..77d93546
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.lightboxinterface.less
@@ -0,0 +1,105 @@
+@import 'mmv.globals';
+@import 'mmv.mixins';
+
+.mw-mmv-wrapper {
+ top: 0;
+ left: 0;
+ right: 0;
+ z-index: 1001;
+ position: absolute;
+ bottom: auto;
+
+ .skin-monobook & {
+ font-size: medium;
+ }
+}
+
+.mw-mmv-main {
+ width: 100%;
+ height: auto;
+ position: relative;
+
+ .jq-fullscreened {
+ background-color: #000;
+ }
+}
+
+.mw-mmv-image-wrapper {
+ position: fixed;
+ top: 0;
+ bottom: @metadatabar-above-fold-height;
+ left: 0;
+ right: 0;
+ overflow-y: hidden;
+}
+
+.mw-mmv-image-inner-wrapper {
+ display: table;
+ width: 100%;
+ height: 100%;
+}
+
+.mw-mmv-pre-image {
+ position: absolute;
+ top: 0;
+ height: 32px;
+ width: 100%;
+ z-index: 1;
+}
+
+.mw-mmv-post-image {
+ position: absolute;
+ width: 100%;
+ bottom: auto;
+ height: auto;
+ color: #222;
+ background-color: @panel-above-fold-background-color;
+ min-height: ( @metadatabar-above-fold-inner-height + 1 );
+ z-index: 1005;
+}
+
+// above-the-fold part of the metadata panel
+.mw-mmv-above-fold {
+ width: 100%;
+ height: @metadatabar-above-fold-inner-height;
+ // min-height is used when the height is changed to auto to display long texts, to make sure the layout
+ // is not messed up wheen the text is short and does not fill the available place. It is also used by
+ // JavaScript to get the "default" height.
+ min-height: @metadatabar-above-fold-inner-height;
+ position: relative;
+ // make sure there is no content in the part which is overlapped by the revealed part of the below-fold content
+ // also used in JavaScript for the height calculations
+ padding-bottom: @metadatabar-below-fold-pushup-height;
+
+ .mw-mmv-untruncated & {
+ height: auto;
+ }
+}
+
+// Fullscreen styles
+
+.cursor-hidden {
+ cursor: none;
+}
+
+.mw-mmv-main.jq-fullscreened {
+ background-color: #000;
+}
+
+.jq-fullscreened {
+ .mw-mmv-image-wrapper, // make the image occupy the whole screen
+ .mw-mmv-post-image { // make sure the panel fits in the screen and does not cause scrollbars to appear
+ bottom: 0;
+ }
+
+ .mw-mmv-post-image {
+ position: fixed;
+ min-height: 0;
+ .opacity( 0 );
+ transition: opacity 0.25s;
+
+ &:hover {
+ .opacity( 1 );
+ }
+ }
+}
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.mixins.less b/www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.mixins.less
new file mode 100644
index 00000000..262d9fdb
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.mixins.less
@@ -0,0 +1,60 @@
+/* stylelint-disable function-parentheses-space-inside */
+/* stylelint-disable string-quotes */
+
+.unselectable() {
+ -webkit-user-select: none;
+ -moz-user-select: -moz-none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+.opacity( @value ) {
+ opacity: @value;
+ filter: e( %( "alpha(opacity=%s )", round( @value * 100 ) ) ); // IE6-8
+ zoom: 1; // IE 6-7 hasLayout hack
+}
+
+// from http://stackoverflow.com/a/12178019/323407
+.fade-out-vertical( @backgroundColor: white ) {
+ @invisible: fadeout( @backgroundColor, 100% );
+ background-image: -moz-linear-gradient( top, @invisible 0%, @backgroundColor 100% ); // FF3.6+
+ background-image: -webkit-gradient( linear, left top, left bottom, color-stop( 0%, @invisible ), color-stop( 100%, @backgroundColor ) ); // Chrome,Safari4+
+ background-image: -webkit-linear-gradient( top, @invisible 0%, @backgroundColor 100% ); // Chrome10+,Safari5.1+
+ background-image: linear-gradient( to bottom, @invisible 0%, @backgroundColor 100% ); // W3C
+ filter: e( %( "progid:DXImageTransform.Microsoft.gradient( startColorstr='#%s', endColorstr='#%s',GradientType=0 )", rgbahex( @invisible ), rgbahex( @backgroundColor) ) ); // IE6-9
+}
+.fade-out-horizontal( @backgroundColor: white ) {
+ @invisible: fadeout( @backgroundColor, 100% );
+ background-image: -moz-linear-gradient( left, @invisible 0%, @backgroundColor 100% ); // FF3.6+
+ background-image: -webkit-gradient( linear, left top, right top, color-stop(0%, @invisible ), color-stop( 100%, @backgroundColor) ); // Chrome,Safari4+
+ background-image: -webkit-linear-gradient( left, @invisible 0%, @backgroundColor 100% ); // Chrome10+,Safari5.1+
+ background-image: linear-gradient( to right, @invisible 0%, @backgroundColor 100% ); // W3C
+ filter: e( %( "progid:DXImageTransform.Microsoft.gradient( startColorstr='#%s', endColorstr='#%s',GradientType=1 )", rgbahex( @invisible ), rgbahex( @backgroundColor) ) ); // IE6-9
+}
+
+.rotate( @degrees: 45deg ) {
+ -webkit-transform: rotate( @degrees );
+ -moz-transform: rotate( @degrees );
+ -ms-transform: rotate( @degrees ); // IE 9 only
+ transform: rotate( @degrees );
+
+ /* stylelint-disable number-no-trailing-zeros */
+ filter: progid:DXImageTransform.Microsoft.BasicImage( rotation=( @degrees / 90.0 ) );
+ -ms-filter: 'progid:DXImageTransform.Microsoft.BasicImage( rotation=( @degrees / 90.0 ) )';
+ /* stylelint-enable number-no-trailing-zeros */
+
+ // The filter rules ( which are for IE < 9 ) cause a bug in IE 9 where the rotated
+ // element will have a black background. So we have to disable them in IE9
+ /* stylelint-disable declaration-block-no-duplicate-properties */
+ filter: none \0;
+ -ms-filter: none \0;
+ /* stylelint-enable declaration-block-no-duplicate-properties */
+}
+
+// from http://pixelhunter.me/post/25782670606/css3-grayscale
+.grayscale() {
+ -webkit-filter: grayscale( 100% );
+ filter: url( "data:image/svg+xml;utf8,<svg xmlns=\'http://www.w3.org/2000/svg\'><filter id=\'grayscale\'><feColorMatrix type=\'matrix\' values=\'0.3333 0.3333 0.3333 0 0 0.3333 0.3333 0.3333 0 0 0.3333 0.3333 0.3333 0 0 0 0 0 1 0\'/></filter></svg>#grayscale" ); /* Firefox 3.5+ */ /* stylelint-disable-line function-url-quotes */
+ filter: grayscale( 100% );
+ filter: #72777d; // IE 6-9
+}
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/model/mmv.model.EmbedFileInfo.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/model/mmv.model.EmbedFileInfo.js
new file mode 100644
index 00000000..3e5378dd
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/model/mmv.model.EmbedFileInfo.js
@@ -0,0 +1,53 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw ) {
+ /**
+ * Contains information needed to embed and share files.
+ *
+ * @class mw.mmv.model.EmbedFileInfo
+ * @constructor
+ * @param {mw.mmv.model.Image} imageInfo
+ * @param {mw.mmv.model.Repo} repoInfo
+ * @param {string} [caption]
+ * @param {string} [alt]
+ */
+ function EmbedFileInfo(
+ imageInfo,
+ repoInfo,
+ caption,
+ alt
+ ) {
+ if ( !imageInfo || !repoInfo ) {
+ throw new Error( 'imageInfo and repoInfo are required and must have a value' );
+ }
+
+ /** @property {mw.mmv.model.Image} imageInfo The title of the file */
+ this.imageInfo = imageInfo;
+
+ /** @property {mw.mmv.model.Repo} repoInfo The URL to the original file */
+ this.repoInfo = repoInfo;
+
+ /** @property {Object} [caption] Image caption, if any */
+ this.caption = caption;
+
+ /** @property {string} [alt] Alt text for image */
+ this.alt = alt;
+ }
+
+ mw.mmv.model.EmbedFileInfo = EmbedFileInfo;
+}( mediaWiki ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/model/mmv.model.Image.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/model/mmv.model.Image.js
new file mode 100644
index 00000000..26498ccf
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/model/mmv.model.Image.js
@@ -0,0 +1,343 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw ) {
+ var IP;
+
+ /**
+ * Represents information about a single image
+ *
+ * @class mw.mmv.model.Image
+ * @constructor
+ * @param {mw.Title} title
+ * @param {string} name Image name (e.g. title of the artwork) or human-readable file if there is no better title
+ * @param {number} size Filesize in bytes of the original image
+ * @param {number} width Width of the original image
+ * @param {number} height Height of the original image
+ * @param {string} mimeType
+ * @param {string} url URL to the image itself (original version)
+ * @param {string} descriptionUrl URL to the image description page
+ * @param {string} descriptionShortUrl A short URL to the description page for the image, using curid=...
+ * @param {string} pageID pageId of the description page for the image
+ * @param {string} repo The repository this image belongs to
+ * @param {string} uploadDateTime The time and date the last upload occurred
+ * @param {string} anonymizedUploadDateTime Anonymized and EL-friendly version of uploadDateTime
+ * @param {string} creationDateTime The time and date the original upload occurred
+ * @param {string} description
+ * @param {string} source
+ * @param {string} author
+ * @param {number} authorCount
+ * @param {mw.mmv.model.License} license
+ * @param {string} permission
+ * @param {string} attribution Custom attribution string that replaces credit line when set
+ * @param {string} deletionReason
+ * @param {number} latitude
+ * @param {number} longitude
+ * @param {string[]} restrictions
+ */
+ function Image(
+ title,
+ name,
+ size,
+ width,
+ height,
+ mimeType,
+ url,
+ descriptionUrl,
+ descriptionShortUrl,
+ pageID,
+ repo,
+ uploadDateTime,
+ anonymizedUploadDateTime,
+ creationDateTime,
+ description,
+ source,
+ author,
+ authorCount,
+ license,
+ permission,
+ attribution,
+ deletionReason,
+ latitude,
+ longitude,
+ restrictions
+ ) {
+ /** @property {mw.Title} title The title of the image file */
+ this.title = title;
+
+ /** @property {string} name Image name (e.g. title of the artwork) or human-readable file if there is no better title */
+ this.name = name;
+
+ /** @property {number} size The filesize, in bytes, of the original image */
+ this.size = size;
+
+ /** @property {number} width The width, in pixels, of the original image */
+ this.width = width;
+
+ /** @property {number} height The height, in pixels, of the original image */
+ this.height = height;
+
+ /** @property {string} mimeType The MIME type of the original image */
+ this.mimeType = mimeType;
+
+ /** @property {string} url The URL to the original image */
+ this.url = url;
+
+ /** @property {string} descriptionUrl The URL to the description page for the image */
+ this.descriptionUrl = descriptionUrl;
+
+ /** @property {string} descriptionShortUrl A short URL to the description page for the image, using curid=... */
+ this.descriptionShortUrl = descriptionShortUrl;
+
+ /** @property {number} pageId of the description page for the image */
+ this.pageID = pageID;
+
+ /** @property {string} repo The name of the repository where this image is stored */
+ this.repo = repo;
+
+ /** @property {string} uploadDateTime The date and time of the last upload */
+ this.uploadDateTime = uploadDateTime;
+
+ /** @property {string} anonymizedUploadDateTime The anonymized date and time of the last upload */
+ this.anonymizedUploadDateTime = anonymizedUploadDateTime;
+
+ /** @property {string} creationDateTime The date and time that the image was created */
+ this.creationDateTime = creationDateTime;
+
+ /** @property {string} description The description from the file page - unsafe HTML sometimes goes here */
+ this.description = description;
+
+ /** @property {string} source The source for the image (could be an organization, e.g.) - unsafe HTML sometimes goes here */
+ this.source = source;
+
+ /** @property {string} author The author of the image - unsafe HTML sometimes goes here */
+ this.author = author;
+
+ /** @property {number} authorCount The number of different authors of the image. This is guessed by the
+ * number of templates with author fields, so might be less than the number of actual authors. */
+ this.authorCount = authorCount;
+
+ /** @property {mw.mmv.model.License} license The license under which the image is distributed */
+ this.license = license;
+
+ /** @property {string} additional license conditions by the author (note that this is usually a big ugly HTML blob) */
+ this.permission = permission;
+
+ /** @property {string} attribution custom attribution string set by uploader that replaces credit line */
+ this.attribution = attribution;
+
+ /** @property {string|null} reason for pending deletion, null if image is not about to be deleted */
+ this.deletionReason = deletionReason;
+
+ /** @property {number} latitude The latitude of the place where the image was created */
+ this.latitude = latitude;
+
+ /** @property {number} longitude The longitude of the place where the image was created */
+ this.longitude = longitude;
+
+ /** @property {string[]} restrictions Any re-use restrictions for the image, eg trademarked */
+ this.restrictions = restrictions;
+
+ /**
+ * @property {Object} thumbUrls
+ * An object indexed by image widths
+ * with URLs to appropriately sized thumbnails
+ */
+ this.thumbUrls = {};
+ }
+ IP = Image.prototype;
+
+ /**
+ * Constructs a new Image object out of an object containing
+ *
+ * imageinfo data from an API response.
+ *
+ * @static
+ * @param {mw.Title} title
+ * @param {Object} imageInfo
+ * @return {mw.mmv.model.Image}
+ */
+ Image.newFromImageInfo = function ( title, imageInfo ) {
+ var name, uploadDateTime, anonymizedUploadDateTime, creationDateTime, imageData,
+ description, source, author, authorCount, license, permission, attribution,
+ deletionReason, latitude, longitude, restrictions,
+ innerInfo = imageInfo.imageinfo[ 0 ],
+ extmeta = innerInfo.extmetadata;
+
+ if ( extmeta ) {
+ creationDateTime = this.parseExtmeta( extmeta.DateTimeOriginal, 'plaintext' );
+ uploadDateTime = this.parseExtmeta( extmeta.DateTime, 'plaintext' ).toString();
+
+ // Convert to "timestamp" format commonly used in EventLogging
+ anonymizedUploadDateTime = uploadDateTime.replace( /[^\d]/g, '' );
+
+ // Anonymise the timestamp to avoid making the file identifiable
+ // We only need to know the day
+ anonymizedUploadDateTime = anonymizedUploadDateTime.substr( 0, anonymizedUploadDateTime.length - 6 ) + '000000';
+
+ name = this.parseExtmeta( extmeta.ObjectName, 'plaintext' );
+
+ description = this.parseExtmeta( extmeta.ImageDescription, 'string' );
+ source = this.parseExtmeta( extmeta.Credit, 'string' );
+ author = this.parseExtmeta( extmeta.Artist, 'string' );
+ authorCount = this.parseExtmeta( extmeta.AuthorCount, 'integer' );
+
+ license = this.newLicenseFromImageInfo( extmeta );
+ permission = this.parseExtmeta( extmeta.Permission, 'string' );
+ attribution = this.parseExtmeta( extmeta.Attribution, 'string' );
+ deletionReason = this.parseExtmeta( extmeta.DeletionReason, 'string' );
+ restrictions = this.parseExtmeta( extmeta.Restrictions, 'list' );
+
+ latitude = this.parseExtmeta( extmeta.GPSLatitude, 'float' );
+ longitude = this.parseExtmeta( extmeta.GPSLongitude, 'float' );
+ }
+
+ if ( !name ) {
+ name = title.getNameText();
+ }
+
+ imageData = new Image(
+ title,
+ name,
+ innerInfo.size,
+ innerInfo.width,
+ innerInfo.height,
+ innerInfo.mime,
+ innerInfo.url,
+ innerInfo.descriptionurl,
+ innerInfo.descriptionshorturl,
+ imageInfo.pageid,
+ imageInfo.imagerepository,
+ uploadDateTime,
+ anonymizedUploadDateTime,
+ creationDateTime,
+ description,
+ source,
+ author,
+ authorCount,
+ license,
+ permission,
+ attribution,
+ deletionReason,
+ latitude,
+ longitude,
+ restrictions
+ );
+
+ if ( innerInfo.thumburl ) {
+ imageData.addThumbUrl(
+ innerInfo.thumbwidth,
+ innerInfo.thumburl
+ );
+ }
+
+ return imageData;
+ };
+
+ /**
+ * Constructs a new License object out of an object containing
+ * imageinfo data from an API response.
+ *
+ * @static
+ * @param {Object} extmeta the extmeta array of the imageinfo data
+ * @return {mw.mmv.model.License|undefined}
+ */
+ Image.newLicenseFromImageInfo = function ( extmeta ) {
+ var license;
+
+ if ( extmeta.LicenseShortName ) {
+ license = new mw.mmv.model.License(
+ this.parseExtmeta( extmeta.LicenseShortName, 'string' ),
+ this.parseExtmeta( extmeta.License, 'string' ),
+ this.parseExtmeta( extmeta.UsageTerms, 'string' ),
+ this.parseExtmeta( extmeta.LicenseUrl, 'string' ),
+ this.parseExtmeta( extmeta.AttributionRequired, 'boolean' ),
+ this.parseExtmeta( extmeta.NonFree, 'boolean' )
+ );
+ }
+
+ return license;
+ };
+
+ /**
+ * Reads and parses a value from the imageinfo API extmetadata field.
+ *
+ * @param {Array} data
+ * @param {string} type one of 'plaintext', 'string', 'float', 'boolean', 'list'
+ * @return {string|number|boolean|Array} value or undefined if it is missing
+ */
+ Image.parseExtmeta = function ( data, type ) {
+ var value = data && data.value;
+ if ( value === null || value === undefined ) {
+ return undefined;
+ } else if ( type === 'plaintext' ) {
+ return value.toString().replace( /<.*?>/g, '' );
+ } else if ( type === 'string' ) {
+ return value.toString();
+ } else if ( type === 'integer' ) {
+ return parseInt( value, 10 );
+ } else if ( type === 'float' ) {
+ return parseFloat( value );
+ } else if ( type === 'boolean' ) {
+ value = value.toString().toLowerCase().replace( /^\s+|\s+$/g, '' );
+ if ( value in { 1: null, yes: null, 'true': null } ) {
+ return true;
+ } else if ( value in { 0: null, no: null, 'false': null } ) {
+ return false;
+ } else {
+ return undefined;
+ }
+ } else if ( type === 'list' ) {
+ return value === '' ? [] : value.split( '|' );
+ } else {
+ throw new Error( 'mw.mmv.model.Image.parseExtmeta: unknown type' );
+ }
+ };
+
+ /**
+ * Add a thumb URL
+ *
+ * @param {number} width
+ * @param {string} url
+ */
+ IP.addThumbUrl = function ( width, url ) {
+ this.thumbUrls[ width ] = url;
+ };
+
+ /**
+ * Get a thumb URL if we have it.
+ *
+ * @param {number} width
+ * @return {string|undefined}
+ */
+ IP.getThumbUrl = function ( width ) {
+ return this.thumbUrls[ width ];
+ };
+
+ /**
+ * Check whether the image has geolocation data.
+ *
+ * @return {boolean}
+ */
+ IP.hasCoords = function () {
+ return this.hasOwnProperty( 'latitude' ) && this.hasOwnProperty( 'longitude' ) &&
+ this.latitude !== undefined && this.latitude !== null &&
+ this.longitude !== undefined && this.longitude !== null;
+ };
+
+ mw.mmv.model.Image = Image;
+}( mediaWiki ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/model/mmv.model.IwTitle.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/model/mmv.model.IwTitle.js
new file mode 100644
index 00000000..046cbf94
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/model/mmv.model.IwTitle.js
@@ -0,0 +1,80 @@
+/*
+ * This file is part of the MediaWiki extension MediaViewer.
+ *
+ * MediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw ) {
+ var ITP;
+
+ /**
+ * IwTitle represents a title in a foreign wiki. The long-term goal is to have an interface
+ * largely compatible with mw.Title, but for now we only implement what we actually need.
+ *
+ * @class mw.mmv.model.IwTitle
+ * @param {string} namespaceId namespace number
+ * @param {string} title full title, including namespace name; with underscores (as in mw.Title#getPrefixedDb())
+ * @param {string} domain domain name of the wiki
+ * @param {string} url full URL to the page
+ * @constructor
+ */
+ function IwTitle(
+ namespaceId,
+ title,
+ domain,
+ url
+ ) {
+ /** @property {number} namespaceId - */
+ this.namespaceId = namespaceId;
+
+ /** @property {string} title - */
+ this.title = title;
+
+ /** @property {string} domain - */
+ this.domain = domain;
+
+ /** @property {string} url - */
+ this.url = url;
+ }
+ ITP = IwTitle.prototype;
+
+ /**
+ * Turn underscores into spaces.
+ * Copy of the private function in mw.Title.
+ *
+ * @param {string} s
+ * @return {string}
+ */
+ function text( s ) {
+ return s ? s.replace( /_/g, ' ' ) : '';
+ }
+
+ ITP.getUrl = function () {
+ return this.url;
+ };
+
+ ITP.getPrefixedDb = function () {
+ return this.title;
+ };
+
+ ITP.getPrefixedText = function () {
+ return text( this.getPrefixedDb() );
+ };
+
+ ITP.getDomain = function () {
+ return this.domain;
+ };
+
+ mw.mmv.model.IwTitle = IwTitle;
+}( mediaWiki ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/model/mmv.model.License.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/model/mmv.model.License.js
new file mode 100644
index 00000000..2e608091
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/model/mmv.model.License.js
@@ -0,0 +1,144 @@
+/*
+ * This file is part of the MediaWiki extension MediaViewer.
+ *
+ * MediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw ) {
+ var LP;
+
+ /**
+ * Class for storing license information about an image. For available fields, see
+ * TemplateParser::$licenseFieldClasses in the CommonsMetadata extension.
+ *
+ * @class mw.mmv.model.License
+ * @param {string} shortName see {@link #shortName}
+ * @param {string} [internalName] see {@link #internalName}
+ * @param {string} [longName] see {@link #longName}
+ * @param {string} [deedUrl] see {@link #deedUrl}
+ * @param {boolean} [attributionRequired] see {@link #attributionRequired}
+ * @param {boolean} [nonFree] see {@link #nonFree}
+ * @constructor
+ */
+ function License(
+ shortName,
+ internalName,
+ longName,
+ deedUrl,
+ attributionRequired,
+ nonFree
+ ) {
+ if ( !shortName ) {
+ throw new Error( 'mw.mmv.model.License: shortName is required' );
+ }
+
+ /** @property {string} shortName short (abbreviated) name of the license (e.g. CC-BY-SA-3.0) */
+ this.shortName = shortName;
+
+ /** @property {string} internalName internal name of the license, used for localization (e.g. cc-by-sa ) */
+ this.internalName = internalName;
+
+ /** @property {string} longName full name of the license (e.g. Creative Commons etc. etc.) */
+ this.longName = longName;
+
+ /** @property {string} deedUrl URL to the description of the license (e.g. the CC deed) */
+ this.deedUrl = deedUrl;
+
+ /** @property {boolean} attributionRequired does the author need to be attributed on reuse? */
+ this.attributionRequired = attributionRequired;
+
+ /** @property {boolean} nonFree is this a non-free license? */
+ this.nonFree = nonFree;
+
+ /** @property {mw.mmv.HtmlUtils} htmlUtils - */
+ this.htmlUtils = new mw.mmv.HtmlUtils();
+ }
+ LP = License.prototype;
+
+ /**
+ * Check whether this is a Creative Commons license.
+ *
+ * @return {boolean}
+ */
+ LP.isCc = function () {
+ return this.internalName ? this.internalName.substr( 0, 2 ) === 'cc' : false;
+ };
+
+ /**
+ * Check whether this is a public domain "license".
+ *
+ * @return {boolean}
+ */
+ LP.isPd = function () {
+ return this.internalName === 'pd';
+ };
+
+ /**
+ * Check whether this is a free license.
+ *
+ * @return {boolean}
+ */
+ LP.isFree = function () {
+ // licenses with missing nonfree information are assumed free
+ return !this.nonFree;
+ };
+
+ /**
+ * Check whether reusers need to attribute the author
+ *
+ * @return {boolean}
+ */
+ LP.needsAttribution = function () {
+ // to be on the safe side, if the attribution required flag is not set, it is assumed to be true
+ return !this.isPd() && this.attributionRequired !== false;
+ };
+
+ /**
+ * Returns the short name of the license:
+ * - if we have interface messages for this license (basically just CC and PD), use those
+ * - otherwise use the short name from the license template (might or might not be translated
+ * still, depending on how the template is set up)
+ *
+ * @return {string}
+ * FIXME a model should not depend on an i18n class. We should probably use view models.
+ */
+ LP.getShortName = function () {
+ var message = 'multimediaviewer-license-' + ( this.internalName || '' );
+ if ( mw.messages.exists( message ) ) {
+ return mw.message( message ).text();
+ } else {
+ return this.shortName;
+ }
+ };
+
+ /**
+ * Returns a short HTML representation of the license.
+ *
+ * @return {string}
+ */
+ LP.getShortLink = function () {
+ var shortName = this.getShortName();
+
+ if ( this.deedUrl ) {
+ return this.htmlUtils.makeLinkText( shortName, {
+ href: this.deedUrl,
+ title: this.longName || shortName
+ } );
+ } else {
+ return shortName;
+ }
+ };
+
+ mw.mmv.model.License = License;
+}( mediaWiki ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/model/mmv.model.Repo.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/model/mmv.model.Repo.js
new file mode 100644
index 00000000..d23ac74b
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/model/mmv.model.Repo.js
@@ -0,0 +1,209 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/* eslint-disable no-use-before-define */
+
+( function ( mw, oo ) {
+ /**
+ * Represents information about a single image repository
+ *
+ * @class mw.mmv.model.Repo
+ * @constructor
+ * @param {string} displayName
+ * @param {string} favIcon URL to the repo's favicon
+ * @param {boolean} isLocal
+ */
+ function Repo(
+ displayName,
+ favIcon,
+ isLocal
+ ) {
+ /** @property {string} displayName Human-readable name of the repository */
+ this.displayName = displayName;
+
+ /** @property {string} favIcon An icon that represents the repository */
+ this.favIcon = favIcon;
+
+ /** @property {boolean} isLocal Whether the repository is the local wiki */
+ this.isLocal = isLocal;
+ }
+
+ /**
+ * Creates a new object from repoInfo we found in an API response.
+ *
+ * @static
+ * @param {Object} repoInfo
+ * @return {mw.mmv.model.Repo}
+ */
+ Repo.newFromRepoInfo = function ( repoInfo ) {
+ if ( repoInfo.apiurl ) {
+ return new ForeignApiRepo(
+ repoInfo.displayname,
+ repoInfo.favicon,
+ false,
+ repoInfo.apiurl,
+ repoInfo.server,
+ repoInfo.articlepath
+ );
+ } else if ( repoInfo.descBaseUrl ) {
+ return new ForeignDbRepo(
+ repoInfo.displayname,
+ repoInfo.favicon,
+ false,
+ repoInfo.descBaseUrl
+ );
+ } else {
+ return new Repo( repoInfo.displayname, repoInfo.favicon, repoInfo.local );
+ }
+ };
+
+ /**
+ * Returns true if the repo is Wikimedia Commons.
+ *
+ * @return {boolean}
+ */
+ Repo.prototype.isCommons = function () {
+ // there does not seem to be a sane way to do this
+ return this.displayName === 'Wikimedia Commons';
+ };
+
+ /**
+ * Gets the article path for the repository.
+ *
+ * @param {boolean} absolute if true, the URL will be absolute (if false, it still might be)
+ * @return {string} Replace $1 with the page name you want to link to.
+ */
+ Repo.prototype.getArticlePath = function ( absolute ) {
+ var articlePath = mw.config.get( 'wgArticlePath' );
+ if ( absolute ) {
+ articlePath = mw.config.get( 'wgServer' ) + articlePath;
+ }
+ return articlePath;
+ };
+
+ /**
+ * Gets the a link to the site where the image was uploaded to.
+ * This is a hack and might break for wikis with exotic config; unfortunately no
+ * better data is provided currently.
+ *
+ * @return {string}
+ */
+ Repo.prototype.getSiteLink = function () {
+ return this.getArticlePath( true ).replace( '$1', '' );
+ };
+
+ /**
+ * Represents information about a foreign API repository
+ *
+ * @class mw.mmv.model.ForeignApiRepo
+ * @extends mw.mmv.model.Repo
+ * @constructor
+ * @inheritdoc
+ * @param {string} displayName
+ * @param {string} favIcon
+ * @param {boolean} isLocal
+ * @param {string} apiUrl URL to the wiki's api.php
+ * @param {string} server Hostname for the wiki
+ * @param {string} articlePath Path to articles on the wiki, relative to the hostname.
+ */
+ function ForeignApiRepo(
+ displayName,
+ favIcon,
+ isLocal,
+ apiUrl,
+ server,
+ articlePath
+ ) {
+ Repo.call( this, displayName, favIcon, isLocal );
+
+ /** @property {string} apiUrl URL to the wiki's api.php */
+ this.apiUrl = apiUrl;
+
+ /** @property {string} server Hostname for the wiki */
+ this.server = server;
+
+ /** @property {string} articlePath Path to articles on the wiki, relative to the hostname */
+ this.articlePath = articlePath;
+
+ /** @property {string} absoluteArticlePath Path to articles on the wiki, relative to nothing */
+ this.absoluteArticlePath = server + articlePath;
+ }
+
+ oo.inheritClass( ForeignApiRepo, Repo );
+
+ /**
+ * @override
+ * @inheritdoc
+ */
+ ForeignApiRepo.prototype.getArticlePath = function () {
+ return this.absoluteArticlePath;
+ };
+
+ /**
+ * @override
+ * @inheritdoc
+ */
+ ForeignApiRepo.prototype.isCommons = function () {
+ return /^(https?:)?\/\/commons.wikimedia.org/.test( this.server );
+ };
+
+ /**
+ * Represents information about a foreign, shared DB repository
+ *
+ * @class mw.mmv.model.ForeignDbRepo
+ * @extends mw.mmv.model.Repo
+ * @constructor
+ * @inheritdoc
+ * @param {string} displayName
+ * @param {string} favIcon
+ * @param {boolean} isLocal
+ * @param {string} descBaseUrl Base URL for description pages - should include the "File:" prefix or similar.
+ */
+ function ForeignDbRepo(
+ displayName,
+ favIcon,
+ isLocal,
+ descBaseUrl
+ ) {
+ Repo.call( this, displayName, favIcon, isLocal );
+
+ /** @property {string} descBaseUrl Base URL for descriptions on the wiki - append a file's title to this to get the description page */
+ this.descBaseUrl = descBaseUrl;
+ }
+
+ oo.inheritClass( ForeignDbRepo, Repo );
+
+ /**
+ * @override
+ * @inheritdoc
+ */
+ ForeignDbRepo.prototype.getArticlePath = function () {
+ return this.descBaseUrl.replace( /[^/:]*:$/, '$1' );
+ };
+
+ /**
+ * @override
+ * @inheritdoc
+ */
+ ForeignDbRepo.prototype.isCommons = function () {
+ return /^(https?:)?\/\/commons.wikimedia.org/.test( this.descBaseUrl );
+ };
+
+ mw.mmv.model.Repo = Repo;
+ mw.mmv.model.ForeignApiRepo = ForeignApiRepo;
+ mw.mmv.model.ForeignDbRepo = ForeignDbRepo;
+}( mediaWiki, OO ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/model/mmv.model.TaskQueue.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/model/mmv.model.TaskQueue.js
new file mode 100644
index 00000000..bfa908bc
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/model/mmv.model.TaskQueue.js
@@ -0,0 +1,141 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $ ) {
+ var tqp;
+
+ /**
+ * A queue which holds a list of tasks (functions). The tasks will be executed in order,
+ * each starting when the previous one has finished (or failed).
+ *
+ * @class mw.mmv.model.TaskQueue
+ * @constructor
+ */
+ function TaskQueue() {
+ /**
+ * The list of functions to execute.
+ * @protected
+ * @property {Array.<function()>}
+ */
+ this.queue = [];
+
+ /**
+ * State of the task queue (running, finished etc)
+ * @protected
+ * @property {mw.mmv.model.TaskQueue.State}
+ */
+ this.state = TaskQueue.State.NOT_STARTED;
+
+ /**
+ * A deferred which shows the state of the queue.
+ * @protected
+ * @property {jQuery.Deferred}
+ */
+ this.deferred = $.Deferred();
+ }
+
+ tqp = TaskQueue.prototype;
+
+ /**
+ * Adds a task. The task should be a function which returns a promise. (Other return values are
+ * permitted, and will be taken to mean that the task has finished already.) The next task will
+ * start when the promise resolves (or rejects).
+ *
+ * Tasks can only be added before the queue is first executed.
+ *
+ * @param {function()} task
+ */
+ tqp.push = function ( task ) {
+ if ( this.state !== TaskQueue.State.NOT_STARTED ) {
+ throw new Error( 'Task queue already started!' );
+ }
+ this.queue.push( task );
+ };
+
+ /**
+ * Execute the queue. The tasks will be performed in order. No more tasks can be added to the
+ * queue.
+ *
+ * @return {jQuery.Promise} a promise which will resolve when the queue execution is finished,
+ * or reject when it is cancelled.
+ */
+ tqp.execute = function () {
+ if ( this.state === TaskQueue.State.NOT_STARTED ) {
+ this.state = TaskQueue.State.RUNNING;
+ this.runNextTask( 0, $.Deferred().resolve() );
+ }
+
+ return this.deferred;
+ };
+
+ /**
+ * Runs the next task once the current one has finished.
+ *
+ * @param {number} index
+ * @param {jQuery.Promise} currentTask
+ */
+ tqp.runNextTask = function ( index, currentTask ) {
+ var taskQueue = this;
+
+ function handleThen() {
+ if ( !taskQueue.queue[ index ] ) {
+ taskQueue.state = TaskQueue.State.FINISHED;
+ taskQueue.queue = []; // just to be sure there are no memory leaks
+ taskQueue.deferred.resolve();
+ return;
+ }
+
+ taskQueue.runNextTask( index + 1, $.when( taskQueue.queue[ index ]() ) );
+ }
+
+ if ( this.state !== TaskQueue.State.RUNNING ) {
+ return;
+ }
+
+ currentTask.then( handleThen, handleThen );
+ };
+
+ /**
+ * Cancel the queue. No more tasks will be executed.
+ */
+ tqp.cancel = function () {
+ this.state = TaskQueue.State.CANCELLED;
+ this.queue = []; // just to be sure there are no memory leaks
+ this.deferred.reject();
+ };
+
+ /**
+ * State of the task queue (running, finished etc)
+ *
+ * @enum {string} mw.mmv.model.TaskQueue.State
+ */
+ TaskQueue.State = {
+ /** not executed yet, tasks can still be added */
+ NOT_STARTED: 'not_started',
+
+ /** some task is being executed */
+ RUNNING: 'running',
+
+ /** all tasks finished, queue can be discarded */
+ FINISHED: 'finished',
+
+ /** cancel() function has been called, queue can be discarded */
+ CANCELLED: 'cancelled'
+ };
+
+ mw.mmv.model.TaskQueue = TaskQueue;
+}( mediaWiki, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/model/mmv.model.Thumbnail.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/model/mmv.model.Thumbnail.js
new file mode 100644
index 00000000..aef05763
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/model/mmv.model.Thumbnail.js
@@ -0,0 +1,48 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw ) {
+ /**
+ * Represents information about an image thumbnail
+ *
+ * @class mw.mmv.model.Thumbnail
+ * @constructor
+ * @param {string} url URL to the thumbnail
+ * @param {number} width Width in pixels
+ * @param {number} height Height in pixels
+ */
+ function Thumbnail(
+ url,
+ width,
+ height
+ ) {
+ if ( !url || !width || !height ) {
+ throw new Error( 'All parameters are required and cannot be empty or zero' );
+ }
+
+ /** @property {string} url The URL to the thumbnail */
+ this.url = url;
+
+ /** @property {number} width The width of the thumbnail in pixels */
+ this.width = width;
+
+ /** @property {number} height The height of the thumbnail in pixels */
+ this.height = height;
+ }
+
+ mw.mmv.model.Thumbnail = Thumbnail;
+}( mediaWiki ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/model/mmv.model.ThumbnailWidth.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/model/mmv.model.ThumbnailWidth.js
new file mode 100644
index 00000000..77df870f
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/model/mmv.model.ThumbnailWidth.js
@@ -0,0 +1,82 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw ) {
+ /**
+ * Represents image width information.
+ *
+ * To utilize caching as much as possible, we use images which are displayed at a slightly
+ * different size than their screen size. The ThumbnailWidth model stores the various types of
+ * sizes and helps avoiding accidental incompatible assignments. (Think of it as a slightly
+ * overcomplicated Hungarian notation)
+ *
+ * @class mw.mmv.model.ThumbnailWidth
+ * @constructor
+ * @param {number} cssWidth width in CSS pixels
+ * @param {number} cssHeight height in CSS pixels
+ * @param {number} screen width in screen pixels
+ * @param {number} real width in real pixels
+ */
+ function ThumbnailWidth( cssWidth, cssHeight, screen, real ) {
+ if ( !cssWidth || !cssHeight || !screen || !real ) {
+ throw new Error( 'All parameters are required and cannot be empty or zero' );
+ }
+
+ /**
+ * Width of the thumbnail on the screen, in CSS pixels. This is the number which can be plugged
+ * into UI code like $element.width(x).
+ *
+ * @property {number}
+ */
+ this.cssWidth = cssWidth;
+
+ /**
+ * Height of the thumbnail on the screen, in CSS pixels. This is the number which can be plugged
+ * into UI code like $element.height(x).
+ *
+ * @property {number}
+ */
+ this.cssHeight = cssHeight;
+
+ /**
+ * Width of the thumbnail on the screen, in device pixels. On most devices this is the same as
+ * the CSS width, but devices with high pixel density displays have multiple screen pixels
+ * in a CSS pixel.
+ *
+ * This value is mostly used internally; for most purposes you will need one of the others.
+ *
+ * @property {number}
+ */
+ this.screen = screen;
+
+ /**
+ * "Real" width of the thumbnail. This is the number you need to use in API requests when
+ * obtaining the thumbnail URL. This is usually larger than the screen width, since
+ * downscaling images via CSS looks OK but upscaling them looks ugly. However, for images
+ * where the full size itself is very small, this can be smaller than the screen width, since
+ * we cannot create a thumbnail which is larger than the original image. (In such cases the
+ * image is just positioned to the center of the intended area and the space around it is
+ * left empty.)
+ *
+ * @property {number}
+ */
+ this.real = real;
+ }
+
+ mw.mmv.model.ThumbnailWidth = ThumbnailWidth;
+
+}( mediaWiki ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/model/mmv.model.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/model/mmv.model.js
new file mode 100644
index 00000000..2c986543
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/model/mmv.model.js
@@ -0,0 +1,20 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw ) {
+ mw.mmv.model = {};
+}( mediaWiki ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/provider/mmv.provider.Api.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/provider/mmv.provider.Api.js
new file mode 100644
index 00000000..b1b209b8
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/provider/mmv.provider.Api.js
@@ -0,0 +1,199 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $ ) {
+ /**
+ * Base class for API-based data providers.
+ *
+ * @class mw.mmv.provider.Api
+ * @abstract
+ * @constructor
+ * @param {mw.Api} api
+ * @param {Object} [options]
+ * @cfg {number} [maxage] cache expiration time, in seconds
+ * Will be used for both client-side cache (maxage) and reverse proxies (s-maxage)
+ */
+ function Api( api, options ) {
+ /**
+ * API object for dependency injection.
+ * @property {mw.Api}
+ */
+ this.api = api;
+
+ /**
+ * Options object; the exact format and meaning is unspecified and could be different
+ * from subclass to subclass.
+ * @property {Object}
+ */
+ this.options = options || {};
+
+ /**
+ * API call cache.
+ * @property {Object.<string, jQuery.Promise>} cache
+ * @protected
+ */
+ this.cache = {};
+ }
+
+ /**
+ * Wraps a caching layer around a function returning a promise; if getCachedPromise has been
+ * called with the same key already, it will return the previous result.
+ *
+ * Since it is the promise and not the API response that gets cached, this method can ensure
+ * that there are no race conditions and multiple calls to the same resource: even if the
+ * request is still in progress, separate calls (with the same key) to getCachedPromise will
+ * share on the same promise object.
+ * The promise is cached even if it is rejected, so if the API request fails, all later calls
+ * to getCachedPromise will fail immediately without retrying the request.
+ *
+ * @param {string} key cache key
+ * @param {function(): jQuery.Promise} getPromise a function to get the promise on cache miss
+ * @return {jQuery.Promise}
+ */
+ Api.prototype.getCachedPromise = function ( key, getPromise ) {
+ var provider = this;
+
+ if ( !this.cache[ key ] ) {
+ this.cache[ key ] = getPromise();
+ this.cache[ key ].fail( function ( error ) {
+ // constructor.name is usually not reliable in inherited classes, but OOjs fixes that
+ mw.log( provider.constructor.name + ' provider failed to load: ', error );
+ } );
+ }
+ return this.cache[ key ];
+ };
+
+ /**
+ * Calls mw.Api.get, with caching parameters.
+ *
+ * @param {Object} params Parameters to the API query.
+ * @param {Object} [ajaxOptions] ajaxOptions argument for mw.Api.get
+ * @param {number|null} [maxage] Cache the call for this many seconds.
+ * Sets both the maxage (client-side) and smaxage (proxy-side) caching parameters.
+ * Null means no caching. Undefined means the default caching period is used.
+ * @return {jQuery.Promise} the return value from mw.Api.get
+ */
+ Api.prototype.apiGetWithMaxAge = function ( params, ajaxOptions, maxage ) {
+ if ( maxage === undefined ) {
+ maxage = this.options.maxage;
+ }
+ if ( maxage ) {
+ params.maxage = params.smaxage = maxage;
+ }
+
+ return this.api.get( params, ajaxOptions );
+ };
+
+ /**
+ * Pulls an error message out of an API response.
+ *
+ * @param {Object} data
+ * @param {Object} data.error
+ * @param {string} data.error.code
+ * @param {string} data.error.info
+ * @return {string} From data.error.code + ': ' + data.error.info, or 'unknown error'
+ */
+ Api.prototype.getErrorMessage = function ( data ) {
+ var errorCode, errorMessage;
+ errorCode = data.error && data.error.code;
+ errorMessage = data.error && data.error.info || 'unknown error';
+ if ( errorCode ) {
+ errorMessage = errorCode + ': ' + errorMessage;
+ }
+ return errorMessage;
+ };
+
+ /**
+ * Returns a fixed a title based on the API response.
+ * The title of the returned file might be different from the requested title, e.g.
+ * if we used a namespace alias. If that happens the old and new title will be set in
+ * data.query.normalized; this method creates an updated title based on that.
+ *
+ * @param {mw.Title} title
+ * @param {Object} data
+ * @return {mw.Title}
+ */
+ Api.prototype.getNormalizedTitle = function ( title, data ) {
+ var i, normalized, length;
+ if ( data && data.query && data.query.normalized ) {
+ for ( normalized = data.query.normalized, length = normalized.length, i = 0; i < length; i++ ) {
+ if ( normalized[ i ].from === title.getPrefixedText() ) {
+ title = new mw.Title( normalized[ i ].to );
+ break;
+ }
+ }
+ }
+ return title;
+ };
+
+ /**
+ * Returns a promise with the specified field from the API result.
+ * This is intended to be used as a .then() callback for action=query APIs.
+ *
+ * @param {string} field
+ * @param {Object} data
+ * @return {jQuery.Promise} when successful, the first argument will be the field data,
+ * when unsuccessful, it will be an error message. The second argument is always
+ * the full API response.
+ */
+ Api.prototype.getQueryField = function ( field, data ) {
+ if ( data && data.query && data.query[ field ] ) {
+ return $.Deferred().resolve( data.query[ field ], data );
+ } else {
+ return $.Deferred().reject( this.getErrorMessage( data ), data );
+ }
+ };
+
+ /**
+ * Returns a promise with the specified page from the API result.
+ * This is intended to be used as a .then() callback for action=query&prop=(...) APIs.
+ *
+ * @param {mw.Title} title
+ * @param {Object} data
+ * @return {jQuery.Promise} when successful, the first argument will be the page data,
+ * when unsuccessful, it will be an error message. The second argument is always
+ * the full API response.
+ */
+ Api.prototype.getQueryPage = function ( title, data ) {
+ var pageName, pageData = null;
+ if ( data && data.query && data.query.pages ) {
+ title = this.getNormalizedTitle( title, data );
+ pageName = title.getPrefixedText();
+
+ // pages is an associative array indexed by pageid,
+ // we need to iterate through to find the right page
+ $.each( data.query.pages, function ( id, page ) {
+ if ( page.title === pageName ) {
+ pageData = page;
+ return false;
+ }
+ } );
+
+ if ( pageData ) {
+ return $.Deferred().resolve( pageData, data );
+ }
+ }
+
+ // If we got to this point either the pages array is missing completely, or we iterated
+ // through it and the requested page was not found. Neither is supposed to happen
+ // (if the page simply did not exist, there would still be a record for it).
+ return $.Deferred().reject( this.getErrorMessage( data ), data );
+ };
+
+ mw.mmv.provider = {};
+ mw.mmv.provider.Api = Api;
+}( mediaWiki, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/provider/mmv.provider.FileRepoInfo.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/provider/mmv.provider.FileRepoInfo.js
new file mode 100644
index 00000000..8a526d62
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/provider/mmv.provider.FileRepoInfo.js
@@ -0,0 +1,63 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, oo, $ ) {
+
+ /**
+ * Gets file repo information.
+ *
+ * @class mw.mmv.provider.FileRepoInfo
+ * @extends mw.mmv.provider.Api
+ * @constructor
+ * @param {mw.Api} api
+ * @param {Object} [options]
+ * @cfg {number} [maxage] cache expiration time, in seconds
+ * Will be used for both client-side cache (maxage) and reverse proxies (s-maxage)
+ */
+ function FileRepoInfo( api, options ) {
+ mw.mmv.provider.Api.call( this, api, options );
+ }
+ oo.inheritClass( FileRepoInfo, mw.mmv.provider.Api );
+
+ /**
+ * Runs an API GET request to get the repo info.
+ *
+ * @return {jQuery.Promise.<Object.<string, mw.mmv.model.Repo>>} a promise which resolves to
+ * a hash of mw.mmv.model.Repo objects, indexed by repo names.
+ */
+ FileRepoInfo.prototype.get = function () {
+ var provider = this;
+
+ return this.getCachedPromise( '*', function () {
+ return provider.apiGetWithMaxAge( {
+ action: 'query',
+ meta: 'filerepoinfo',
+ uselang: 'content'
+ } ).then( function ( data ) {
+ return provider.getQueryField( 'repos', data );
+ } ).then( function ( reposArray ) {
+ var reposHash = {};
+ $.each( reposArray, function ( i, repo ) {
+ reposHash[ repo.name ] = mw.mmv.model.Repo.newFromRepoInfo( repo );
+ } );
+ return reposHash;
+ } );
+ } );
+ };
+
+ mw.mmv.provider.FileRepoInfo = FileRepoInfo;
+}( mediaWiki, OO, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/provider/mmv.provider.GuessedThumbnailInfo.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/provider/mmv.provider.GuessedThumbnailInfo.js
new file mode 100644
index 00000000..5ad76dbd
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/provider/mmv.provider.GuessedThumbnailInfo.js
@@ -0,0 +1,307 @@
+/*
+ * This file is part of the MediaWiki extension MediaViewer.
+ *
+ * MediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $ ) {
+ /**
+ * This provider is similar to mw.mmv.provider.ThumbnailInfo, but instead of making an API call
+ * to get the thumbnail URL, it tries to guess it. There are two failure modes:
+ * - known failure: in the given situation it does not seem possible or safe to guess the URL.
+ * It is up to the caller to obtain it by falling back to the normal provider.
+ * - unexpected failure: we guess an URL but it does not work. The current implementation is
+ * conservative so at least on WMF wikis this probably won't happen, but should be reckoned
+ * with anyway. On other wikis (especially ones which do not generate thumbnails on demand
+ * via the 404 handler) this could be more frequent. Again, it is the caller's resonsibility
+ * to handle this by detecting image loading errors and falling back to the normal provider.
+ *
+ * @class mw.mmv.provider.GuessedThumbnailInfo
+ * @constructor
+ */
+ function GuessedThumbnailInfo() {}
+
+ /**
+ * File extensions which are vector types (as opposed to bitmap).
+ * Thumbnails of vector types can be larger than the original file.
+ * @property {Object.<string, number>}
+ */
+ GuessedThumbnailInfo.prototype.vectorExtensions = {
+ svg: 1
+ };
+
+ /**
+ * File extensions which can be displayed in the browser.
+ * Other file types need to be thumbnailed even if the size of the original file would be right.
+ * @property {Object.<string, number>}
+ */
+ GuessedThumbnailInfo.prototype.displayableExtensions = {
+ png: 1,
+ jpg: 1,
+ jpeg: 1,
+ gif: 1
+ };
+
+ /**
+ * Try to guess the thumbnailinfo for a thumbnail without doing an API request.
+ * An existing thumbnail URL is required.
+ *
+ * There is no guarantee this function will be successful - in some cases, it is impossible
+ * to guess how the URL would look. If that's the case, the promise just rejects.
+ *
+ * @param {mw.Title} file
+ * @param {string} sampleUrl a thumbnail URL for the same file (but with different size).
+ * @param {number} width thumbnail width in pixels
+ * @param {number} originalWidth width of original image in pixels
+ * @param {number} originalHeight height of original image in pixels
+ * @return {jQuery.Promise.<mw.mmv.model.Thumbnail>}
+ */
+ GuessedThumbnailInfo.prototype.get = function ( file, sampleUrl, width, originalWidth, originalHeight ) {
+ var url = this.getUrl( file, sampleUrl, width, originalWidth );
+ if ( url ) {
+ return $.Deferred().resolve( new mw.mmv.model.Thumbnail(
+ url,
+ this.guessWidth( file, width, originalWidth ),
+ this.guessHeight( file, width, originalWidth, originalHeight )
+ ) );
+ } else {
+ return $.Deferred().reject( 'Could not guess thumbnail URL' );
+ }
+ };
+
+ /**
+ * Try to guess the URL of a thumbnail without doing an API request.
+ * See #get().
+ *
+ * @param {mw.Title} file
+ * @param {string} sampleUrl a thumbnail URL for the same file (but with different size)
+ * @param {number} width thumbnail width in pixels
+ * @param {number} originalWidth width of original image in pixels
+ * @return {string|undefined} a thumbnail URL or nothing
+ */
+ GuessedThumbnailInfo.prototype.getUrl = function ( file, sampleUrl, width, originalWidth ) {
+ var needsFullSize = this.needsOriginal( file, width, originalWidth ),
+ sampleIsFullSize = this.isFullSizeUrl( sampleUrl, file );
+
+ if ( sampleIsFullSize && needsFullSize ) {
+ // sample thumbnail uses full size, and we need full size as well - the sample URL
+ // happens to be just the right one for us
+ return sampleUrl;
+ } else if ( !sampleIsFullSize && !needsFullSize ) {
+ // need to convert a scaled thumbnail URL to another scaled thumbnail URL
+ return this.replaceSize( file, sampleUrl, width );
+ } else if ( !sampleIsFullSize && needsFullSize ) {
+ if ( this.canBeDisplayedInBrowser( file ) ) {
+ // the size requested is larger than the original - we need to return an URL
+ // to the original file instead
+ return this.guessFullUrl( file, sampleUrl );
+ } else {
+ // the size requested is larger than the original, but this file type cannot
+ // be displayed by all browsers, so needs to be thumbnailed anyway,
+ // but the thumbnail still cannot be larger than the original file
+ return this.replaceSize( file, sampleUrl, originalWidth );
+ }
+ } else { // sampleIsFullSize && !needsOriginal
+ return this.guessThumbUrl( file, sampleUrl, width );
+ }
+ };
+
+ /**
+ * True if the the original image needs to be used as a thumbnail.
+ *
+ * @protected
+ * @param {mw.Title} file
+ * @param {number} width thumbnail width in pixels
+ * @param {number} originalWidth width of original image in pixels
+ * @return {boolean}
+ */
+ GuessedThumbnailInfo.prototype.needsOriginal = function ( file, width, originalWidth ) {
+ return width >= originalWidth && !this.canHaveLargerThumbnailThanOriginal( file );
+ };
+
+ /**
+ * Checks if a given thumbnail URL is full-size (the original image) or scaled
+ *
+ * @protected
+ * @param {string} url a thumbnail URL
+ * @param {mw.Title} file
+ * @return {boolean}
+ */
+ GuessedThumbnailInfo.prototype.isFullSizeUrl = function ( url, file ) {
+ return !this.obscureFilename( url, file ).match( '/thumb/' );
+ };
+
+ /**
+ * Removes the filename in a reversible way. This is useful because the filename can be nearly
+ * anything and could cause false positives when looking for patterns.
+ *
+ * @protected
+ * @param {string} url a thumbnail URL
+ * @param {mw.Title} file
+ * @return {string} thumbnnail URL with occurences of the filename replaced by `<filename>`
+ */
+ GuessedThumbnailInfo.prototype.obscureFilename = function ( url, file ) {
+ // corresponds to File::getUrlRel() which uses rawurlencode()
+ var filenameInUrl = mw.util.rawurlencode( file.getMain() );
+
+ // In the URL to the original file the filename occurs once. In a thumbnail URL it usually
+ // occurs twice, but can occur once if it is too short. We replace twice, can't hurt.
+ return url.replace( filenameInUrl, '<filename>' ).replace( filenameInUrl, '<filename>' );
+ };
+
+ /**
+ * Undoes #obscureFilename().
+ *
+ * @protected
+ * @param {string} url a thumbnail URL (with obscured filename)
+ * @param {mw.Title} file
+ * @return {string} original thumbnnail URL
+ */
+ GuessedThumbnailInfo.prototype.restoreFilename = function ( url, file ) {
+ // corresponds to File::getUrlRel() which uses rawurlencode()
+ var filenameInUrl = mw.util.rawurlencode( file.getMain() );
+
+ // <> cannot be used in titles, so this is safe
+ return url.replace( '<filename>', filenameInUrl ).replace( '<filename>', filenameInUrl );
+ };
+
+ /**
+ * True if the file is of a type for which the thumbnail can be scaled beyond the original size.
+ *
+ * @protected
+ * @param {mw.Title} file
+ * @return {boolean}
+ */
+ GuessedThumbnailInfo.prototype.canHaveLargerThumbnailThanOriginal = function ( file ) {
+ return ( file.getExtension().toLowerCase() in this.vectorExtensions );
+ };
+
+ /**
+ * True if the file type can be displayed in most browsers, false if it needs thumbnailing
+ *
+ * @protected
+ * @param {mw.Title} file
+ * @return {boolean}
+ */
+ GuessedThumbnailInfo.prototype.canBeDisplayedInBrowser = function ( file ) {
+ return ( file.getExtension().toLowerCase() in this.displayableExtensions );
+ };
+
+ /**
+ * Guess what will be the width of the thumbnail. (Thumbnails for most file formats cannot be
+ * larger than the original file so this might be smaller than the requested width.)
+ *
+ * @protected
+ * @param {mw.Title} file
+ * @param {number} width thumbnail width in pixels
+ * @param {number} originalWidth width of original image in pixels
+ * @return {number} guessed width
+ */
+ GuessedThumbnailInfo.prototype.guessWidth = function ( file, width, originalWidth ) {
+ if ( width >= originalWidth && !this.canHaveLargerThumbnailThanOriginal( file ) ) {
+ return originalWidth;
+ } else {
+ return width;
+ }
+ };
+
+ /**
+ * Guess what will be the height of the thumbnail, given its width.
+ *
+ * @protected
+ * @param {mw.Title} file
+ * @param {number} width thumbnail width in pixels
+ * @param {number} originalWidth width of original image in pixels
+ * @param {number} originalHeight height of original image in pixels
+ * @return {number} guessed height
+ */
+ GuessedThumbnailInfo.prototype.guessHeight = function ( file, width, originalWidth, originalHeight ) {
+ if ( width >= originalWidth && !this.canHaveLargerThumbnailThanOriginal( file ) ) {
+ return originalHeight;
+ } else {
+ // might be off 1px due to rounding (we don't know what exact scaling method the
+ // backend uses) but that should not cause any issues
+ return Math.round( width * ( originalHeight / originalWidth ) );
+ }
+ };
+
+ /**
+ * Given a thumbnail URL with a wrong size, returns one with the right size.
+ *
+ * @protected
+ * @param {mw.Title} file
+ * @param {string} sampleUrl a thumbnail URL for the same file (but with different size)
+ * @param {number} width thumbnail width in pixels
+ * @return {string|undefined} thumbnail URL with the correct size
+ */
+ GuessedThumbnailInfo.prototype.replaceSize = function ( file, sampleUrl, width ) {
+ var url = this.obscureFilename( sampleUrl, file ),
+ sizeRegexp = /\b\d{1,5}px\b/;
+
+ // this should never happen, but let's play it safe - returning the sample URL and believing
+ // it is the resized one would be bad. Returning a wrong filename is not catastrophical
+ // as long as we return a non-working wrong filename, which would not be the case here.
+ if ( !url.match( sizeRegexp ) ) {
+ return undefined;
+ }
+
+ // we are assuming here that the other thumbnail parameters do not look like a size
+ url = url.replace( sizeRegexp, width + 'px' );
+
+ return this.restoreFilename( url, file );
+ };
+
+ /**
+ * Try to guess the original URL to the file, from a thumb URL.
+ *
+ * @protected
+ * @param {mw.Title} file
+ * @param {string} thumbnailUrl
+ * @return {string} URL of the original file
+ */
+ GuessedThumbnailInfo.prototype.guessFullUrl = function ( file, thumbnailUrl ) {
+ var url = this.obscureFilename( thumbnailUrl, file );
+
+ if ( url === thumbnailUrl ) {
+ // Did not find the filename, maybe due to URL encoding issues. Bail out.
+ return undefined;
+ }
+
+ // this depends on some config settings, but will work with default or WMF settings.
+ url = url.replace( /<filename>.*/, '<filename>' );
+ url = url.replace( '/thumb', '' );
+
+ return this.restoreFilename( url, file );
+ };
+
+ /**
+ * Hardest version: try to guess thumbnail URL from original
+ *
+ * @protected
+ * @param {mw.Title} file
+ * @param {string} originalUrl URL for the original file
+ * @param {number} width thumbnail width in pixels
+ * @return {string|undefined} thumbnail URL
+ */
+ GuessedThumbnailInfo.prototype.guessThumbUrl = function () {
+ // Not implemented. This can be very complicated (the thumbnail might have other
+ // parameters than the size, which are impossible to guess, might be converted to some
+ // other format, might have a special shortened format depending on the length of the
+ // filename) and it is unlikely to be useful - it would be only called when we need
+ // a thumbnail that is smaller than the sample (the thumbnail which is already on the page).
+ return undefined;
+ };
+
+ mw.mmv.provider.GuessedThumbnailInfo = GuessedThumbnailInfo;
+}( mediaWiki, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/provider/mmv.provider.Image.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/provider/mmv.provider.Image.js
new file mode 100644
index 00000000..9a20edfd
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/provider/mmv.provider.Image.js
@@ -0,0 +1,153 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $ ) {
+
+ /**
+ * Loads an image.
+ *
+ * @class mw.mmv.provider.Image
+ * @constructor
+ * @param {string} imageQueryParameter When defined, is a query parameter to add to every image request
+ */
+ function Image( imageQueryParameter ) {
+ /**
+ * @property {mw.mmv.logging.PerformanceLogger}
+ * @private
+ */
+ this.performance = new mw.mmv.logging.PerformanceLogger();
+
+ this.imageQueryParameter = imageQueryParameter;
+
+ /**
+ * AJAX call cache.
+ * @property {Object.<string, jQuery.Promise>} cache
+ * @protected
+ */
+ this.cache = {};
+ }
+
+ /**
+ * Loads an image and returns it. Includes performance metrics via mw.mmv.logging.PerformanceLogger.
+ * When the browser supports it, the image is loaded as an AJAX request.
+ *
+ * @param {string} url
+ * @param {jQuery.Deferred.<string>} extraStatsDeferred A promise which resolves to extra statistics.
+ * @return {jQuery.Promise.<HTMLImageElement>} A promise which resolves to the image object.
+ * When loaded via AJAX, it has progress events, which return an array with the content loaded
+ * so far and with the progress as a floating-point number between 0 and 100.
+ */
+ Image.prototype.get = function ( url, extraStatsDeferred ) {
+ var provider = this,
+ cacheKey = url,
+ extraParam = {},
+ start,
+ rawGet,
+ uri;
+
+ if ( this.imageQueryParameter ) {
+ uri = new mw.Uri( url );
+ extraParam[ this.imageQueryParameter ] = null;
+ url = uri.extend( extraParam ).toString();
+ }
+
+ if ( !this.cache[ cacheKey ] ) {
+ if ( this.imagePreloadingSupported() ) {
+ rawGet = $.proxy( provider.rawGet, provider, url, true );
+ this.cache[ cacheKey ] = this.performance.record( 'image', url, extraStatsDeferred ).then( rawGet, rawGet );
+ } else {
+ start = $.now();
+ this.cache[ cacheKey ] = this.rawGet( url );
+ this.cache[ cacheKey ].always( function () {
+ provider.performance.recordEntry( 'image', $.now() - start, url, undefined, extraStatsDeferred );
+ } );
+ }
+ this.cache[ cacheKey ].fail( function ( error ) {
+ mw.log( provider.constructor.name + ' provider failed to load: ', error );
+ } );
+ }
+
+ return this.cache[ cacheKey ];
+ };
+
+ /**
+ * Internal version of get(): no caching, no performance metrics.
+ *
+ * @param {string} url
+ * @param {boolean} [cors] if true, use CORS for preloading
+ * @return {jQuery.Promise.<HTMLImageElement>} a promise which resolves to the image object
+ */
+ Image.prototype.rawGet = function ( url, cors ) {
+ var img = new window.Image(),
+ deferred = $.Deferred();
+
+ // This attribute is necessary in Firefox, which needs it for the image request after
+ // the XHR to hit the cache by being a proper CORS request. In IE11, however,
+ // the presence of that attribute would cause the second image request to miss the cache,
+ // because IE11 adds a no-cache request header to image CORS requests. As a result,
+ // we call needsCrossOrigin to check if the current browser needs to set the attribute
+ // or not in order to avoid loading the image twice.
+ if ( cors && this.needsCrossOrigin() ) {
+ img.crossOrigin = 'anonymous';
+ }
+
+ img.onload = function () {
+ deferred.resolve( img );
+ };
+ img.onerror = function () {
+ deferred.reject( 'could not load image from ' + url );
+ };
+
+ img.src = url;
+
+ return deferred;
+ };
+
+ /**
+ * Checks whether the current browser supports AJAX preloading of images.
+ * This means that:
+ * - the browser supports CORS requests (large wiki farms usually host images on a
+ * separate domain) and
+ * - either AJAX and normal image loading uses the same cache (when an image is used by a CORS
+ * request, and then normally by setting img.src, it is only loaded once)
+ * - or (as is the case with Firefox) they are cached separately, but that can be changed by
+ * setting the crossOrigin attribute
+ *
+ * @return {boolean}
+ */
+ Image.prototype.imagePreloadingSupported = function () {
+ // This checks if the browser supports CORS requests in XHRs
+ return window.XMLHttpRequest !== undefined && 'withCredentials' in new XMLHttpRequest();
+ };
+
+ /**
+ * Checks whether the current browser needs to set crossOrigin on images to avoid
+ * doing a double load
+ *
+ * @return {boolean} Browser needs to set crossOrigin
+ */
+ Image.prototype.needsCrossOrigin = function () {
+ // Support: IE11
+ // This check is essentially "is this browser anything but IE > 10?".
+ // I couldn't find something more topical because IE11 does support the crossOrigin
+ // attribute, just in a counter-productive way compared to all the other browsers
+ // who also support it.
+ return window.MSInputMethodContext === undefined;
+ };
+
+ mw.mmv.provider.Image = Image;
+}( mediaWiki, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/provider/mmv.provider.ImageInfo.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/provider/mmv.provider.ImageInfo.js
new file mode 100644
index 00000000..9973fe99
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/provider/mmv.provider.ImageInfo.js
@@ -0,0 +1,117 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, oo, $ ) {
+
+ /**
+ * Gets file information.
+ *
+ * See https://www.mediawiki.org/wiki/API:Properties#imageinfo_.2F_ii
+ *
+ * @class mw.mmv.provider.ImageInfo
+ * @extends mw.mmv.provider.Api
+ * @constructor
+ * @param {mw.Api} api
+ * @param {Object} [options]
+ * @cfg {string} [language=null] image metadata language
+ * @cfg {number} [maxage] cache expiration time, in seconds
+ * Will be used for both client-side cache (maxage) and reverse proxies (s-maxage)
+ */
+ function ImageInfo( api, options ) {
+ options = $.extend( {
+ language: null
+ }, options );
+
+ mw.mmv.provider.Api.call( this, api, options );
+ }
+ oo.inheritClass( ImageInfo, mw.mmv.provider.Api );
+
+ /**
+ * List of imageinfo API properties which are needed to construct an Image model.
+ *
+ * @property {string}
+ */
+ ImageInfo.prototype.iiprop = [
+ 'timestamp',
+ 'url',
+ 'size',
+ 'mime',
+ 'mediatype',
+ 'extmetadata'
+ ].join( '|' );
+
+ /**
+ * List of imageinfo extmetadata fields which are needed to construct an Image model.
+ *
+ * @property {string}
+ */
+ ImageInfo.prototype.iiextmetadatafilter = [
+ 'DateTime',
+ 'DateTimeOriginal',
+ 'ObjectName',
+ 'ImageDescription',
+ 'License',
+ 'LicenseShortName',
+ 'UsageTerms',
+ 'LicenseUrl',
+ 'Credit',
+ 'Artist',
+ 'AuthorCount',
+ 'GPSLatitude',
+ 'GPSLongitude',
+ 'Permission',
+ 'Attribution',
+ 'AttributionRequired',
+ 'NonFree',
+ 'Restrictions',
+ 'DeletionReason'
+ ].join( '|' );
+
+ /**
+ * Runs an API GET request to get the image info.
+ *
+ * @param {mw.Title} file
+ * @return {jQuery.Promise} a promise which resolves to an mw.mmv.model.Image object.
+ */
+ ImageInfo.prototype.get = function ( file ) {
+ var provider = this;
+
+ return this.getCachedPromise( file.getPrefixedDb(), function () {
+ return provider.apiGetWithMaxAge( {
+ action: 'query',
+ prop: 'imageinfo',
+ titles: file.getPrefixedDb(),
+ iiprop: provider.iiprop,
+ iiextmetadatafilter: provider.iiextmetadatafilter,
+ iiextmetadatalanguage: provider.options.language,
+ uselang: 'content'
+ } ).then( function ( data ) {
+ return provider.getQueryPage( file, data );
+ } ).then( function ( page ) {
+ if ( page.imageinfo && page.imageinfo.length ) {
+ return mw.mmv.model.Image.newFromImageInfo( file, page );
+ } else if ( page.missing === '' && page.imagerepository === '' ) {
+ return $.Deferred().reject( 'file does not exist: ' + file.getPrefixedDb() );
+ } else {
+ return $.Deferred().reject( 'unknown error' );
+ }
+ } );
+ } );
+ };
+
+ mw.mmv.provider.ImageInfo = ImageInfo;
+}( mediaWiki, OO, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/provider/mmv.provider.ThumbnailInfo.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/provider/mmv.provider.ThumbnailInfo.js
new file mode 100644
index 00000000..45b6cdef
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/provider/mmv.provider.ThumbnailInfo.js
@@ -0,0 +1,89 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, oo, $ ) {
+
+ /**
+ * Gets thumbnail information.
+ *
+ * See https://www.mediawiki.org/wiki/API:Properties#imageinfo_.2F_ii
+ *
+ * @class mw.mmv.provider.ThumbnailInfo
+ * @extends mw.mmv.provider.Api
+ * @constructor
+ * @param {mw.Api} api
+ * @param {Object} [options]
+ * @cfg {number} [maxage] cache expiration time, in seconds
+ * Will be used for both client-side cache (maxage) and reverse proxies (s-maxage)
+ */
+ function ThumbnailInfo( api, options ) {
+ mw.mmv.provider.Api.call( this, api, options );
+ }
+ oo.inheritClass( ThumbnailInfo, mw.mmv.provider.Api );
+
+ /**
+ * Runs an API GET request to get the thumbnail info for the specified size.
+ * The thumbnail always has the same aspect ratio as the full image.
+ * One of width or height can be null; if both are set, the API will return the largest
+ * thumbnail which fits into a width x height bounding box (or the full-sized image - whichever
+ * is smaller).
+ *
+ * @param {mw.Title} file
+ * @param {number} width thumbnail width in pixels
+ * @param {number} height thumbnail height in pixels
+ * @return {jQuery.Promise.<mw.mmv.model.Thumbnail>}
+ */
+ ThumbnailInfo.prototype.get = function ( file, width, height ) {
+ var provider = this,
+ cacheKey = file.getPrefixedDb() + '|' + ( width || '' ) + '|' + ( height || '' );
+
+ return this.getCachedPromise( cacheKey, function () {
+ return provider.apiGetWithMaxAge( {
+ action: 'query',
+ prop: 'imageinfo',
+ titles: file.getPrefixedDb(),
+ iiprop: 'url',
+ iiurlwidth: width, // mw.Api will omit null/undefined parameters
+ iiurlheight: height
+ } ).then( function ( data ) {
+ return provider.getQueryPage( file, data );
+ } ).then( function ( page ) {
+ var imageInfo;
+ if ( page.imageinfo && page.imageinfo[ 0 ] ) {
+ imageInfo = page.imageinfo[ 0 ];
+ if ( imageInfo.thumburl && imageInfo.thumbwidth && imageInfo.thumbheight ) {
+ return $.Deferred().resolve(
+ new mw.mmv.model.Thumbnail(
+ imageInfo.thumburl,
+ imageInfo.thumbwidth,
+ imageInfo.thumbheight
+ )
+ );
+ } else {
+ return $.Deferred().reject( 'error in provider, thumb info not found' );
+ }
+ } else if ( page.missing === '' && page.imagerepository === '' ) {
+ return $.Deferred().reject( 'file does not exist: ' + file.getPrefixedDb() );
+ } else {
+ return $.Deferred().reject( 'unknown error' );
+ }
+ } );
+ } );
+ };
+
+ mw.mmv.provider.ThumbnailInfo = ThumbnailInfo;
+}( mediaWiki, OO, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/routing/mmv.routing.MainFileRoute.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/routing/mmv.routing.MainFileRoute.js
new file mode 100644
index 00000000..391a7703
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/routing/mmv.routing.MainFileRoute.js
@@ -0,0 +1,30 @@
+/*
+ * This file is part of the MediaWiki extension MediaViewer.
+ *
+ * MediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, oo ) {
+ /**
+ * Route for showing the main image on the page, (whatever that means might depend on the page).
+ * This is typically used on file pages.
+ *
+ * @class mw.mmv.routing.MainFileRoute
+ * @extends mw.mmv.routing.Route
+ * @constructor
+ */
+ function MainFileRoute() {}
+ oo.inheritClass( MainFileRoute, mw.mmv.routing.Route );
+ mw.mmv.routing.MainFileRoute = MainFileRoute;
+}( mediaWiki, OO ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/routing/mmv.routing.Route.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/routing/mmv.routing.Route.js
new file mode 100644
index 00000000..4985dd81
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/routing/mmv.routing.Route.js
@@ -0,0 +1,28 @@
+/*
+ * This file is part of the MediaWiki extension MediaViewer.
+ *
+ * MediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw ) {
+ /**
+ * The base class for routes. Route classes don't really do anything, they are just simple
+ * containers which specify a certain way of referencing images.
+ *
+ * @class mw.mmv.routing.Route
+ * @constructor
+ */
+ function Route() {}
+ mw.mmv.routing.Route = Route;
+}( mediaWiki ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/routing/mmv.routing.Router.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/routing/mmv.routing.Router.js
new file mode 100644
index 00000000..093bbb6a
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/routing/mmv.routing.Router.js
@@ -0,0 +1,197 @@
+/*
+ * This file is part of the MediaWiki extension MediaViewer.
+ *
+ * MediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw ) {
+ var RP;
+
+ /**
+ * Converts between routes and their URL hash representations such as `mediaviewer/File:Foo`.
+ *
+ * @class mw.mmv.routing.Router
+ * @constructor
+ */
+ function Router() {}
+ RP = Router.prototype;
+
+ /**
+ * The prefix originally used to namespace MediaViewer routing hashes. Since there are many links
+ * out there pointing to those URLs, we should keep them working.
+ *
+ * @protected
+ * @property {string}
+ */
+ RP.legacyPrefix = 'mediaviewer';
+
+ /**
+ * The prefix used to namespace MediaViewer routing hashes
+ *
+ * @protected
+ * @property {string}
+ */
+ RP.applicationPrefix = '/media';
+
+ /**
+ * Takes an URL hash and returns a route (or null if it could not be parsed).
+ * Returns null for URL hashes which were not created by MediaViewer; you should use
+ * #isMediaViewerHash() if you want to differentiate such hashes.
+ * The hash can contain the starting `#` but does not have to; it should be in raw (percent-
+ * encoded) form. Note that the percent-encoding behavior of location.hash is not consistent
+ * between browsers; location.href can be used instead.
+ *
+ * @param {string} hash
+ * @return {mw.mmv.routing.Route|null}
+ */
+ RP.parseHash = function ( hash ) {
+ var hashParts, fileName;
+
+ hashParts = this.tokenizeHash( hash );
+
+ if ( hashParts.length === 0 ) {
+ return null;
+ } else if ( hashParts.length === 1 ) {
+ return new mw.mmv.routing.MainFileRoute();
+ } else if ( hashParts.length === 2 ) {
+ fileName = this.decodeRouteComponent( hashParts[ 1 ] );
+ return new mw.mmv.routing.ThumbnailRoute( new mw.Title( fileName ) );
+ }
+
+ return null;
+ };
+
+ /**
+ * Takes a route and returns a string representation which can be used in the URL fragment.
+ * The string does not contain the starting `#`, and it is encoded and guaranteed to be a
+ * valid URL.
+ *
+ * @param {mw.mmv.routing.Route} route
+ * @return {string}
+ */
+ RP.createHash = function ( route ) {
+ if ( route instanceof mw.mmv.routing.ThumbnailRoute ) {
+ return this.applicationPrefix + '/' +
+ this.encodeRouteComponent( 'File:' + route.fileTitle.getMain() );
+ } else if ( route instanceof mw.mmv.routing.MainFileRoute ) {
+ return this.applicationPrefix;
+ } else if ( route instanceof mw.mmv.routing.Route ) {
+ throw new Error( 'mw.mmv.routing.Router.createHash: not implemented for ' + route.constructor.name );
+ } else {
+ throw new Error( 'mw.mmv.routing.Router.createHash: invalid argument' );
+ }
+ };
+
+ /**
+ * Like #parseHash(), but takes a window.location object. This is a helper function to make
+ * sure that hashes are decoded correctly in spite of browser inconsistencies.
+ *
+ * @param {{href: string}} location window.location object
+ * @return {mw.mmv.routing.Route|null}
+ */
+ RP.parseLocation = function ( location ) {
+ // Firefox percent-decodes location.hash: https://bugzilla.mozilla.org/show_bug.cgi?id=483304
+ // which would cause inconsistent cross-browser behavior for files which have % or /
+ // characters in their names. Using location.href is safe.
+ return this.parseHash( location.href.split( '#' )[ 1 ] || '' );
+ };
+
+ /**
+ * Like #createHash(), but appends the hash to a specified URL
+ *
+ * @param {mw.mmv.routing.Route} route
+ * @param {string} baseUrl the URL of the page the image is on (can contain a hash part,
+ * which will be stripped)
+ * @return {string} an URL to the same page as baseUrl, with the hash for the given route
+ */
+ RP.createHashedUrl = function ( route, baseUrl ) {
+ return baseUrl.replace( /#.*/, '' ) + '#' + this.createHash( route );
+ };
+
+ /**
+ * Returns true if this hash looks like it was created by MediaViewer.
+ * The hash can contain the starting `#` but does not have to.
+ *
+ * @param {string} hash
+ * @return {boolean}
+ */
+ RP.isMediaViewerHash = function ( hash ) {
+ return this.tokenizeHash( hash ).length !== 0;
+ };
+
+ /**
+ * Returns "segments" of a hash. The first segment is always the #applicationPrefix.
+ * If the hash is not a MediaViewer routing hash, an empty array is returned.
+ * The input hash can contain the starting `#` but does not have to.
+ *
+ * @protected
+ * @param {string} hash
+ * @return {string[]}
+ */
+ RP.tokenizeHash = function ( hash ) {
+ var prefix,
+ hashParts;
+
+ if ( hash[ 0 ] === '#' ) {
+ hash = hash.slice( 1 );
+ }
+
+ if ( hash.indexOf( this.legacyPrefix ) === 0 ) {
+ prefix = this.legacyPrefix;
+ }
+
+ if ( hash.indexOf( this.applicationPrefix ) === 0 ) {
+ prefix = this.applicationPrefix;
+ }
+
+ if ( prefix === undefined ) {
+ return [];
+ }
+
+ hash = hash.slice( prefix.length );
+
+ hashParts = hash.split( '/' );
+ hashParts[ 0 ] = prefix;
+
+ return hashParts;
+ };
+
+ /**
+ * URL-encodes a route component.
+ * Almost identical to mw.util.wikiUrlencode but makes sure there are no unencoded `/`
+ * characters left since we use those to delimit components.
+ *
+ * @protected
+ * @param {string} component
+ * @return {string}
+ */
+ RP.encodeRouteComponent = function ( component ) {
+ return mw.util.wikiUrlencode( component ).replace( /\//g, '%2F' );
+ };
+
+ /**
+ * URL-decodes a route component.
+ * This is basically just a standard percent-decode, but for backwards compatibility with
+ * older schemes, we also replace spaces which underlines (the current scheme never has spaces).
+ *
+ * @protected
+ * @param {string} component
+ * @return {string}
+ */
+ RP.decodeRouteComponent = function ( component ) {
+ return decodeURIComponent( component ).replace( / /g, '_' );
+ };
+
+ mw.mmv.routing.Router = Router;
+}( mediaWiki ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/routing/mmv.routing.ThumbnailRoute.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/routing/mmv.routing.ThumbnailRoute.js
new file mode 100644
index 00000000..328ad8c9
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/routing/mmv.routing.ThumbnailRoute.js
@@ -0,0 +1,36 @@
+/*
+ * This file is part of the MediaWiki extension MediaViewer.
+ *
+ * MediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, oo ) {
+ /**
+ * Route for a specific thumbnail on the current page. The thumbnail must be that of a wiki
+ * file (can't be e.g. an external image); can be a file from a remote repo though.
+ *
+ * @class mw.mmv.routing.ThumbnailRoute
+ * @extends mw.mmv.routing.Route
+ * @constructor
+ * @param {mw.Title} fileTitle the name of the image
+ */
+ function ThumbnailRoute( fileTitle ) {
+ if ( !fileTitle ) {
+ throw new Error( 'mw.mmv.routing.ThumbnailRoute: fileTitle parameter is required' );
+ }
+ this.fileTitle = fileTitle;
+ }
+ oo.inheritClass( ThumbnailRoute, mw.mmv.routing.Route );
+ mw.mmv.routing.ThumbnailRoute = ThumbnailRoute;
+}( mediaWiki, OO ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/routing/mmv.routing.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/routing/mmv.routing.js
new file mode 100644
index 00000000..34785f0a
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/routing/mmv.routing.js
@@ -0,0 +1,20 @@
+/*
+ * This file is part of the MediaWiki extension MediaViewer.
+ *
+ * MediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw ) {
+ mw.mmv.routing = {};
+}( mediaWiki ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/checker.png b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/checker.png
new file mode 100644
index 00000000..3e9e3d09
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/checker.png
Binary files differ
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/cc.svg b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/cc.svg
new file mode 100644
index 00000000..998f30a0
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/cc.svg
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="5.5 -3.5 64 64">
+ <circle cx="37.785" cy="28.501" r="28.836" fill="none" stroke="#72777d"/>
+ <path fill="#72777d" d="M37.441-3.5c8.951 0 16.572 3.125 22.857 9.372 3.008 3.009 5.295 6.448 6.857 10.314 1.561 3.867 2.344 7.971 2.344 12.314 0 4.381-.773 8.486-2.314 12.313-1.543 3.828-3.82 7.21-6.828 10.143-3.123 3.085-6.666 5.448-10.629 7.086-3.961 1.638-8.057 2.457-12.285 2.457s-8.276-.808-12.143-2.429c-3.866-1.618-7.333-3.961-10.4-7.027-3.067-3.066-5.4-6.524-7-10.372S5.5 32.767 5.5 28.5c0-4.229.809-8.295 2.428-12.2 1.619-3.905 3.972-7.4 7.057-10.486C21.08-.394 28.565-3.5 37.441-3.5zm.116 5.772c-7.314 0-13.467 2.553-18.458 7.657-2.515 2.553-4.448 5.419-5.8 8.6a25.204 25.204 0 0 0-2.029 9.972c0 3.429.675 6.734 2.029 9.913 1.353 3.183 3.285 6.021 5.8 8.516 2.514 2.496 5.351 4.399 8.515 5.715a25.652 25.652 0 0 0 9.943 1.971c3.428 0 6.75-.665 9.973-1.999 3.219-1.335 6.121-3.257 8.713-5.771 4.99-4.876 7.484-10.99 7.484-18.344 0-3.543-.648-6.895-1.943-10.057-1.293-3.162-3.18-5.98-5.654-8.458-5.146-5.143-11.335-7.715-18.573-7.715zm-.401 20.915l-4.287 2.229c-.458-.951-1.019-1.619-1.685-2-.667-.38-1.286-.571-1.858-.571-2.856 0-4.286 1.885-4.286 5.657 0 1.714.362 3.084 1.085 4.113.724 1.029 1.791 1.544 3.201 1.544 1.867 0 3.181-.915 3.944-2.743l3.942 2c-.838 1.563-2 2.791-3.486 3.686-1.484.896-3.123 1.343-4.914 1.343-2.857 0-5.163-.875-6.915-2.629-1.752-1.752-2.628-4.19-2.628-7.313 0-3.048.886-5.466 2.657-7.257 1.771-1.79 4.009-2.686 6.715-2.686 3.963-.002 6.8 1.541 8.515 4.627zm18.457 0l-4.229 2.229c-.457-.951-1.02-1.619-1.686-2-.668-.38-1.307-.571-1.914-.571-2.857 0-4.287 1.885-4.287 5.657 0 1.714.363 3.084 1.086 4.113.723 1.029 1.789 1.544 3.201 1.544 1.865 0 3.18-.915 3.941-2.743l4 2c-.875 1.563-2.057 2.791-3.541 3.686a9.233 9.233 0 0 1-4.857 1.343c-2.896 0-5.209-.875-6.941-2.629-1.736-1.752-2.602-4.19-2.602-7.313 0-3.048.885-5.466 2.658-7.257 1.77-1.79 4.008-2.686 6.713-2.686 3.962-.002 6.783 1.541 8.458 4.627z"/>
+</svg>
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/commons_white.svg b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/commons_white.svg
new file mode 100644
index 00000000..aa7f0262
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/commons_white.svg
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80">
+ <g fill="#fff">
+ <path d="M39.022 80c-8.302-.252-15.975-4.025-21.384-10.314-4.528-5.283-6.918-11.95-6.918-18.994 0-7.044 2.39-13.71 7.044-19.12.629-.754 2.138-2.264 2.264-2.264l3.9 3.9 3.899 3.899 1.132-1.006c.629-.63 1.132-1.007 1.132-1.007s.754 2.39 2.264 8.05c0 0 0 .126-.126.126 0 0-7.421-2.012-7.924-2.138h-.126s.503-.503 1.006-1.132a11.797 11.797 0 0 0 1.006-1.132c0-.126-1.886-2.013-1.886-2.013s-.504.63-1.007 1.132a20.56 20.56 0 0 0-4.276 10.566c-.126 1.007-.126 1.133 0 1.258h2.767v-1.51c0-1.509 0-1.509.126-1.509.125 0 7.17 4.151 7.17 4.151s0 .126-.126.126c-.126-.126-7.044 3.774-7.044 3.774v-3.02h-2.768v.378c0 .755.252 2.139.503 3.145.63 3.019 2.013 5.786 3.9 8.176.377.377 1.006 1.258 1.006 1.258s2.013-1.887 2.013-2.013c0 0-.503-.503-1.007-1.132-.628-.629-1.006-1.132-1.006-1.132 0-.126 8.05-2.264 8.05-2.138l-2.138 8.05s-.377-.377-1.132-1.006l-1.132-1.132c-.126 0-2.013 1.887-1.887 2.012 0 .126.755.755 1.384 1.132 2.39 1.761 5.157 3.145 8.176 3.774 1.006.252 2.39.377 3.145.503h.377v-2.767H36.38c0-.126 4.151-7.296 4.151-7.296s4.15 7.17 4.15 7.296h-3.018v2.767h.377c.755 0 2.139-.251 3.145-.503 3.019-.629 5.786-2.013 8.176-3.774a23.22 23.22 0 0 0 1.384-1.132c.126-.125-1.887-2.012-1.887-2.012l-1.132 1.132a11.797 11.797 0 0 1-1.132 1.006c0-.126-2.138-8.05-2.138-8.05l8.05 2.138s-.378.378-1.006 1.132c-.63.63-1.007 1.132-1.007 1.132 0 .126 1.887 2.013 2.013 2.013.126 0 .755-.755 1.006-1.258 1.887-2.39 3.145-5.283 3.9-8.176.251-1.006.377-2.39.377-3.145v-.377h-3.522v3.019s-6.918-3.9-7.17-4.151l-.126-.126s7.044-4.15 7.17-4.15c.126 0 .126 0 .126 1.509v1.509s2.641.126 2.767 0v-1.258a20.56 20.56 0 0 0-4.277-10.566c-.503-.629-1.006-1.132-1.006-1.132l-1.887 1.887s.503.503 1.007 1.132a11.826 11.826 0 0 1 1.006 1.132l-.126.126c-.503.126-7.924 2.138-7.924 2.138s-.126 0 0-.126c1.509-5.534 2.138-7.924 2.264-8.05 0 0 .503.377 1.132 1.006l1.132 1.007 1.006-1.007 1.006-1.006s-.628-.503-1.132-.88c-1.132-.755-1.886-1.007-5.534-2.516-2.516-1.007-3.774-1.635-5.283-2.642-3.27-2.264-5.409-5.157-6.793-9.308-.125-.503-.629-2.013-.629-2.013 0-.125-.251 0-2.012.63-2.264 1.131-3.145 1.509-3.396 1.509-.378 0-.504-.126-.252-.378.126-.251.252-.377.629-.88.88-1.132 1.635-2.138 2.516-3.9 1.51-2.767 3.019-6.54 4.276-10.817.378-1.258.88-2.893 1.007-3.27 0-.126 0-.252.125-.252 0 0 3.648 6.164 5.41 9.057 3.396 6.037 4.905 9.182 4.528 9.434-.126.125-.252 0-.88-.63-.881-.88-1.385-1.131-2.265-1.509-.377-.126-1.006-.251-1.384-.251-.377 0-.377 0-.251.503.503 2.012 1.383 3.522 2.515 4.528 1.133 1.132 2.516 1.887 6.038 3.27 2.767 1.133 4.528 2.013 6.038 3.02 1.258.88 2.264 1.635 3.27 2.767 6.29 6.415 9.183 15.094 8.176 24.025-.88 7.799-5.031 14.843-11.195 19.622-3.522 2.642-7.42 4.529-11.698 5.41-2.264.502-4.905.754-7.17.754z"/>
+ <path d="M39.399 60.503c-1.51-.126-2.516-.377-3.648-1.006-1.132-.503-1.887-1.132-2.767-2.013-1.384-1.51-2.39-3.27-2.642-5.409-.125-.754-.125-2.138 0-2.767.126-1.006.378-1.887.755-2.641a9.816 9.816 0 0 1 7.547-5.66c.378 0 .63-.126 1.384-.126.755 0 1.132 0 1.384.125a9.816 9.816 0 0 1 7.547 5.66c.377.881.629 1.636.754 2.642.126.63.126 2.139 0 2.767-.125 1.007-.377 1.887-.88 2.768-.503 1.132-1.006 1.887-1.761 2.641-.88.88-1.635 1.51-2.767 2.013-1.384.629-2.642 1.006-4.151 1.006h-.755z"/>
+ </g>
+</svg>
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/ellipsis_darkgray.svg b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/ellipsis_darkgray.svg
new file mode 100644
index 00000000..d8837049
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/ellipsis_darkgray.svg
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96">
+ <g fill="#54595d" transform="translate(0 20)">
+ <path d="M22 48.3c0 6.1-4.9 11-11 11s-11-4.9-11-11 4.9-11 11-11 11 4.9 11 11zm37 0c0 6.1-4.9 11-11 11s-11-4.9-11-11 4.9-11 11-11 11 4.9 11 11z"/>
+ <circle cx="85" cy="48.3" r="11"/>
+ </g>
+</svg>
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/ellipsis_gray.svg b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/ellipsis_gray.svg
new file mode 100644
index 00000000..bccbc71a
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/ellipsis_gray.svg
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96">
+ <g fill="#72777d" transform="translate(0 20)">
+ <path d="M22 48.3c0 6.1-4.9 11-11 11s-11-4.9-11-11 4.9-11 11-11 11 4.9 11 11zm37 0c0 6.1-4.9 11-11 11s-11-4.9-11-11 4.9-11 11-11 11 4.9 11 11z"/>
+ <circle cx="85" cy="48.3" r="11"/>
+ </g>
+</svg>
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/ellipsis_lightgray.svg b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/ellipsis_lightgray.svg
new file mode 100644
index 00000000..bccbc71a
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/ellipsis_lightgray.svg
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96">
+ <g fill="#72777d" transform="translate(0 20)">
+ <path d="M22 48.3c0 6.1-4.9 11-11 11s-11-4.9-11-11 4.9-11 11-11 11 4.9 11 11zm37 0c0 6.1-4.9 11-11 11s-11-4.9-11-11 4.9-11 11-11 11 4.9 11 11z"/>
+ <circle cx="85" cy="48.3" r="11"/>
+ </g>
+</svg>
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/error-media-icon.svg b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/error-media-icon.svg
new file mode 100644
index 00000000..5e7232ca
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/error-media-icon.svg
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 67.684 79.813">
+ <path fill="#c8ccd1" d="M67.255 0L3.26.816l.535 41.997L19.14 27.07 31.176 38.8l15.306-15.7 16.152 15.745 5.05-5.18z"/>
+ <path fill="#fff" d="M61.357 8.076L8.36 8.752l.37 28.997 10.41-10.68 3.75 3.654 3.015-4.172 8.866 8.56 11.712-12.014L61.738 37.97z"/>
+ <path fill="#c8ccd1" d="M61.638 50.217L47.46 32.673 30.406 46.457l-10.563-13.07-17.1 13.818L0 73.06l63.643 6.753 3.622-34.144z"/>
+ <path fill="#fff" d="M60.852 49.244l-13.39-16.57-13.05 10.545 3.15 3.858 7.21-4.71 8.77 12.238-42.095-4.466L23.13 37.458l-3.288-4.07-11.6 9.373-1.16 10.94 52.704 5.59z"/>
+</svg>
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/file.svg b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/file.svg
new file mode 100644
index 00000000..119cceba
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/file.svg
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="19" height="14" viewBox="0 0 19 14">
+ <path fill="#72777d" d="M13 0H0v14h19V6h-6V0zm4.5 12.3V12H2.4l4.5-5.5.7-.1 4.2 4.5 2.3-1.5 3.4 2.9z"/>
+ <path fill="#72777d" d="M18.8 4.5h-4.5V0"/>
+</svg>
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/gear.svg b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/gear.svg
new file mode 100644
index 00000000..09e2d9e1
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/gear.svg
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96">
+ <path fill-opacity=".51" d="M39.063.038c-.014.004.013.136 0 .14-.014.005-.128-.005-.141 0-.014.005-.128-.005-.14 0-.014.006.012.135 0 .14-.014.007-.128-.005-.141 0-.013.007-.128-.006-.141 0-.013.008.013.134 0 .141-.013.008-.128-.007-.14 0-.013.008-.13-.008-.141 0-.012.009.011.133 0 .141-.012.009-.13-.009-.14 0-.012.009.01.131 0 .14-.012.01-.13-.009-.142 0-.01.01.01.131 0 .141-.01.01-.13-.01-.14 0l-.14.14c-.01.012.008.13 0 .142-.01.01-.133-.012-.141 0-.009.011.008.128 0 .14-.008.012-.133-.012-.141 0-.008.012.008.129 0 .14-.007.013.007.129 0 .141-.007.013-.134-.012-.14 0-.007.013.006.128 0 .141-.006.013-.136.127-.141.14-.006.014.005.128 0 .141-.005.014.004.127 0 .14-.004.015.004.128 0 .142-.004.013-.137-.014-.14 0-.004.013.002.126 0 .14-.004.014.002.127 0 .14-.003.015.001.127 0 .141l-1.407 8.438c-1.472.498-2.851 1.025-4.219 1.687l-7.172-4.921c-.013-.01-.127.008-.14 0-.014-.009-.127-.133-.141-.141-.014-.008-.126.007-.14 0-.015-.007-.127.007-.141 0-.015-.007.015-.134 0-.14-.015-.007-.126.005-.14 0-.016-.006-.126.004-.142 0-.015-.006-.125-.137-.14-.141-.015-.005-.125.003-.14 0-.017-.004-.126.002-.141 0-.016-.003-.125.002-.141 0-.016-.002-.125.001-.14 0h-.141c-.016 0-.125-.14-.14-.141-.017 0-.126.14-.142.14-.015.002-.124 0-.14 0-.016.002-.125-.001-.14 0-.017.003-.126-.002-.141 0-.016.004-.125-.003-.141 0-.015.005-.125-.004-.14 0-.016.005-.126.136-.141.141-.015.006-.126-.005-.14 0-.016.006-.127-.006-.142 0-.014.007.015.134 0 .14-.014.008-.126-.006-.14 0-.014.008-.127-.007-.14 0-.015.009.013.133 0 .142-.014.008-.128-.01-.141 0-.013.009-.128.13-.141.14-.013.01-.128.13-.14.14-.012.011-.13-.01-.141 0-.012.012.011.13 0 .141L8.547 20.006c-.011.012-.13-.011-.14 0-.012.012.01.129 0 .14-.011.013-.131.129-.141.142-.01.013-.132.127-.141.14-.009.014.009.127 0 .14-.008.014-.133-.013-.14 0-.009.015.007.127 0 .141-.008.015.007.127 0 .141-.008.014-.135-.014-.141 0-.007.015.006.126 0 .14-.006.015.005.126 0 .141-.005.016-.136.126-.14.14-.005.016.003.126 0 .142-.005.015.003.125 0 .14-.004.016.001.125 0 .14-.003.017 0 .125 0 .141-.002.016 0 .125 0 .141-.002.016-.141.125-.141.14 0 .017.14.125.14.141v.14c.002.017-.002.126 0 .142.002.015-.002.124 0 .14.003.016-.003.125 0 .14.004.016-.004.126 0 .141.005.016.136.126.14.141.006.015-.005.126 0 .14.007.015-.006.126 0 .141.007.015.135-.014.141 0 .007.015-.007.127 0 .14.008.015-.007.128 0 .142.008.013.133.126.141.14.009.014-.009.128 0 .14l4.922 6.891c-.724 1.484-1.275 3.006-1.828 4.641l-8.297 1.266c-.016.002-.124-.003-.14 0-.017.003-.125-.004-.141 0-.017.004-.125-.005-.141 0-.016.004.016.136 0 .14-.016.005-.125-.005-.14 0-.016.006-.126-.006-.141 0-.016.007-.126-.006-.14 0-.016.007.014.134 0 .14-.016.008-.127-.006-.142 0-.014.009-.126-.007-.14 0-.015.009.014.133 0 .142-.015.008-.127-.009-.14 0-.015.009-.128.13-.141.14-.013.01-.128-.01-.141 0-.013.01.013.13 0 .14-.013.011-.128-.01-.14 0-.013.012.012.13 0 .141-.013.012-.13-.011-.141 0-.012.013.011.129 0 .141-.012.012-.13-.013-.14 0-.012.013.01.128 0 .14-.011.014-.131-.012-.141 0-.01.014.01.128 0 .141-.01.014-.132.127-.141.14-.009.015.008.127 0 .142-.008.014-.133.125-.14.14-.008.015.006.125 0 .14-.008.016-.135-.015-.141 0-.007.016.005.126 0 .141-.006.016.005.125 0 .141-.005.016.004.125 0 .14-.004.017-.137.125-.141.141-.003.016.003.125 0 .14-.003.017.002.125 0 .142-.002.016.002.124 0 .14v17.015c.002.017-.002.125 0 .141.002.017-.003.125 0 .141.003.016-.003.124 0 .14.004.017.137.125.14.141.005.016-.004.125 0 .14.006.017-.005.126 0 .142.006.015-.006.125 0 .14.007.015.135-.015.141 0 .007.015-.007.126 0 .14.008.016.133.127.14.141.009.015-.008.127 0 .141.01.014.132.127.141.14.01.014-.01.128 0 .141.01.013.13-.013.141 0 .01.013-.01.128 0 .14.01.013.13-.011.14 0 .012.013-.01.13 0 .142.012.011.13-.012.141 0 .013.01-.012.129 0 .14.013.01.128-.01.141 0 .013.01-.013.13 0 .14.013.011.127-.01.14 0 .014.01.127.132.141.141.014.009.127-.008.14 0 .015.009-.014.133 0 .141.015.008.127-.008.141 0 .015.007.126-.007.141 0 .015.007-.015.134 0 .14.015.007.125-.006.14 0 .017.006.126-.005.141 0 .016.006.125-.004.141 0 .016.005-.016.137 0 .141.016.005.125-.004.14 0 .017.004.125-.003.141 0 .016.003.124-.002.14 0l8.298 1.407c.55 1.61 1.102 3.14 1.828 4.64l-4.922 6.89c-.009.014.009.128 0 .141-.008.014-.133.127-.14.141-.008.014.007.126 0 .14-.008.015.006.127 0 .141-.007.015-.135-.015-.141 0-.007.015.006.126 0 .14-.006.016.005.126 0 .141-.005.016-.136.126-.14.141-.005.015.003.125 0 .14-.004.016.002.126 0 .141-.003.016.001.125 0 .141-.003.016 0 .125 0 .14-.002.016 0 .125 0 .141-.001.016-.141.125-.141.14 0 .017.14.126.14.141v.141c.002.016-.002.125 0 .14.002.016-.003.126 0 .141.004.016-.004.125 0 .141.004.015-.004.125 0 .14.005.016.136.126.14.141.006.015-.005.126 0 .14.007.016-.006.127 0 .141.008.015.134-.014.141 0 .008.015-.007.127 0 .141.008.014-.008.127 0 .14.008.015.133-.013.141 0 .009.014-.009.128 0 .141.009.013.131.128.14.141.01.013.131.128.141.14.011.012-.01.13 0 .141.011.012.13-.011.14 0L20.22 87.928c.011.011-.012.13 0 .14.011.012.128-.01.14 0 .013.011.128.131.141.141.013.01.127.132.14.141.014.009.128-.009.141 0 .014.008-.014.133 0 .14.014.009.127-.007.14 0 .015.008.127-.007.142 0 .014.008-.015.135 0 .141.014.007.125-.006.14 0 .015.006.126-.005.14 0 .016.005.126.136.141.14.016.005.126-.003.141 0 .016.005.125-.003.14 0 .016.004.125-.001.141 0 .016.003.125 0 .14 0 .017.002.126 0 .142 0 .015.002.124.141.14.141.016 0 .125-.14.14-.14h.141c.016-.002.125.002.141 0 .016-.002.125.002.14 0 .016-.003.126.003.141 0 .016-.004.125.004.14 0 .016-.005.126-.136.142-.14.015-.006.125.005.14 0 .015-.007.126.006.14 0 .016-.007-.014-.135 0-.141.015-.007.127.007.141 0 .014-.008.127.007.141 0 .014-.008.127-.133.14-.141.014-.009.128.009.141 0l6.89-4.922c1.456.69 2.936 1.177 4.5 1.688l1.407 8.437c.003.016-.003.124 0 .14.003.017-.003.125 0 .141.004.017-.004.125 0 .141.004.016.136-.016.14 0 .005.016-.004.125 0 .14.006.016-.005.126 0 .141.007.016-.006.126 0 .14.007.016.135-.014.141 0 .007.016-.007.127 0 .141.008.015-.008.127 0 .141.008.015.133-.014.141 0 .009.015-.009.127 0 .14.009.015.131.128.14.141.01.013-.01.128 0 .141.011.013.13-.013.141 0 .01.013-.01.128 0 .14.012.013.13-.012.14 0 .012.013-.01.13 0 .141.013.012.13-.011.142 0 .012.011-.013.13 0 .14.012.012.127-.01.14 0 .013.011-.013.131 0 .141.013.01.127-.01.14 0 .015.01.127.132.141.141.015.009.127-.008.141 0 .014.008.126.133.14.14.016.007.126-.006.141 0 .016.007-.015.135 0 .141.016.007.125-.005.14 0 .017.006.126-.005.142 0 .015.005.124-.004.14 0 .016.004.125.137.14.141.017.003.125-.003.141 0 .017.003.125-.002.141 0 .017.002.124-.002.14 0h17.156c.018-.002.125.002.142 0 .016-.002.124.003.14 0 .016-.003.125.003.14 0 .017-.004.125-.137.141-.14.017-.005.125.004.141 0 .016-.006.125.005.14 0 .016-.006.126.006.141 0 .016-.007-.015-.135 0-.141.016-.007.126.006.14 0 .016-.008.127-.133.142-.14.014-.009.126.008.14 0 .014-.01.127-.132.14-.142.014-.01.128.01.141 0 .013-.01-.013-.13 0-.14.013-.01.128.01.141 0 .013-.01-.012-.13 0-.14.012-.012.129.01.14 0 .012-.012-.01-.13 0-.141.012-.013.13.012.141 0 .011-.013-.01-.128 0-.141.01-.013.13.013.14 0 .011-.013-.009-.127 0-.14.01-.014.132-.127.142-.141.008-.014-.01-.127 0-.14.008-.015.132.014.14 0 .008-.015-.008-.127 0-.142.007-.014-.007-.125 0-.14.007-.015.134.015.14 0 .007-.015-.006-.125 0-.14.007-.017-.005-.126 0-.141.006-.016-.004-.125 0-.141.005-.016.137.016.141 0 .005-.016-.004-.125 0-.14.004-.017-.003-.125 0-.141.003-.016-.002-.124 0-.14l1.407-8.438c1.542-.547 2.968-1.124 4.359-1.829l7.031 4.922c.013.01.128-.009.14 0 .014.008.128.133.141.141.014.008.127-.007.141 0 .015.007.126-.007.14 0 .015.007-.014.134 0 .14.016.007.126-.005.141 0 .015.006.126-.004.141 0 .015.006.125.137.14.141.016.004.126-.004.141 0 .016.003.125-.003.14 0 .017.003.126-.002.141 0 .016.002.125-.001.141 0h.14c.017 0 .125.14.141.14.017 0 .125-.14.141-.14.016 0 .125.002.14 0 .016-.001.125.002.141 0 .016-.002.125.003.14 0 .017-.003.126.004.141 0 .016-.004.126.005.141 0 .015-.004.126-.135.14-.14.016-.006.126.005.141 0 .015-.006.126.006.141 0 .014-.007-.014-.134 0-.141.014-.007.126.007.14 0 .015-.008.127.008.141 0 .014-.008-.014-.132 0-.14.014-.01.128.008.14 0 .014-.01.129-.132.141-.141.013-.01.129-.13.141-.14.012-.012.13.01.14 0 .012-.012-.01-.13 0-.142L88 76.117c.011-.012.13.011.14 0 .011-.012-.01-.13 0-.141.011-.012.131-.128.141-.14.01-.014.132-.128.14-.141.01-.014-.008-.127 0-.14.009-.015.134.013.141 0 .009-.015-.007-.127 0-.142.008-.014-.007-.126 0-.14.008-.015.134.014.141 0 .006-.015-.006-.126 0-.14.006-.016-.005-.126 0-.141.005-.016.136-.126.14-.141.005-.015-.003-.125 0-.14.004-.016-.003-.126 0-.141.003-.016-.002-.125 0-.14.003-.017 0-.126 0-.142.002-.015 0-.124 0-.14.001-.016.141-.125.141-.14 0-.017-.14-.125-.14-.141v-.141c-.002-.016.002-.125 0-.14-.002-.016.002-.125 0-.141-.003-.016.003-.125 0-.14-.004-.016.004-.126 0-.142-.005-.015-.136-.125-.14-.14-.006-.015.005-.126 0-.14-.007-.016.005-.127 0-.141-.007-.015-.135.014-.141 0-.007-.015.007-.127 0-.141-.008-.014.007-.127 0-.14-.009-.015-.133-.128-.141-.141-.01-.014.009-.128 0-.14L83.5 65.286c.71-1.43 1.274-2.901 1.828-4.5l8.438-1.406c.016-.002.124.003.14 0 .016-.003.125.004.14 0 .017-.004.125.005.141 0 .017-.004-.015-.136 0-.14.016-.005.125.005.141 0 .016-.006.125.005.14 0 .016-.007.126.006.141 0 .016-.007-.015-.134 0-.141.016-.007.126.007.141 0 .015-.008.126.008.14 0 .015-.008-.014-.132 0-.14.015-.01.127.008.141 0 .014-.01.127-.132.14-.141.014-.01.128.01.141 0 .014-.01-.012-.13 0-.14.013-.011.129.01.141 0 .012-.012-.012-.13 0-.142.012-.01.129.012.14 0 .012-.012-.01-.128 0-.14.012-.012.13.012.141 0 .011-.013-.01-.128 0-.14.01-.014.13.012.141 0 .01-.014-.01-.128 0-.141.009-.014.132-.127.14-.141.01-.014-.007-.126 0-.14.009-.015.134-.126.141-.141.007-.016-.007-.126 0-.14.007-.016.135.014.14 0 .007-.016-.005-.126 0-.142.006-.015-.005-.124 0-.14.006-.016-.004-.125 0-.14.005-.017.138-.125.141-.141.004-.017-.003-.125 0-.141.003-.016-.002-.124 0-.14.002-.017-.001-.125 0-.141.002-.017 0-.124 0-.14V39.693c0-.017.002-.124 0-.14-.001-.018.002-.125 0-.142-.002-.016.003-.124 0-.14-.003-.016.004-.125 0-.14-.003-.017-.136-.125-.14-.141-.005-.017.005-.125 0-.141-.006-.016.005-.125 0-.14-.006-.016.006-.126 0-.141-.006-.016-.134.015-.14 0-.007-.016.006-.126 0-.14-.008-.016-.133-.127-.141-.142-.008-.014.008-.126 0-.14-.01-.014-.132-.127-.141-.14-.01-.014.01-.128 0-.141-.01-.013-.13.013-.14 0-.011-.013.01-.128 0-.141-.011-.013-.13.012-.141 0-.011-.012.011-.129 0-.14-.012-.012-.129.01-.14 0-.013-.012.011-.13 0-.141-.013-.011-.129.01-.141 0-.013-.01.013-.13 0-.14-.014-.011-.128.009-.141 0-.014-.01-.127-.132-.14-.142-.015-.008-.127.009-.141 0-.015-.008.014-.132 0-.14-.015-.008-.126.008-.141 0-.015-.007-.125.007-.14 0-.016-.007.015-.134 0-.14-.016-.007-.126.006-.141 0-.016-.007-.125.005-.14 0-.017-.006-.126.004-.141 0-.016-.005.016-.137 0-.141-.017-.005-.125.004-.141 0-.016-.004-.124.003-.14 0-.017-.003-.125.002-.141 0l-8.438-1.407a37.056 37.056 0 0 0-1.828-4.359l4.922-7.031c.009-.013-.01-.128 0-.14.008-.014.132-.128.14-.142.008-.013-.007-.126 0-.14.008-.015-.006-.126 0-.14.007-.015.135.014.141 0 .006-.016-.006-.126 0-.141.006-.015-.005-.126 0-.141.005-.015.136-.125.14-.14.005-.016-.003-.126 0-.141.004-.016-.002-.125 0-.14.003-.017-.001-.126 0-.142.003-.015 0-.124 0-.14.002-.016 0-.125 0-.14.001-.017.141-.125.141-.141 0-.017-.14-.125-.14-.141v-.14c-.002-.016.002-.125 0-.141-.003-.016.002-.125 0-.14-.004-.017.003-.126 0-.142-.004-.015.004-.125 0-.14-.005-.015-.136-.126-.14-.14-.006-.016.005-.126 0-.141-.007-.015.005-.126 0-.141-.008-.014-.135.014-.141 0-.008-.014.007-.126 0-.14-.008-.015.008-.127 0-.141-.009-.014-.133.013-.141 0-.009-.014.009-.128 0-.14-.01-.014-.131-.129-.14-.142-.01-.012-.131-.128-.141-.14-.011-.012.01-.13 0-.14-.011-.012-.13.01-.141 0L76.469 8.333c-.011-.01.011-.13 0-.14-.012-.011-.129.01-.14 0-.013-.01-.129-.131-.141-.14-.014-.01-.128-.132-.141-.142-.014-.008-.127.009-.14 0-.015-.008.013-.132 0-.14-.015-.008-.127.007-.141 0-.015-.007-.127.007-.141 0-.014-.007.014-.134 0-.14-.015-.007-.126.005-.14 0-.015-.006-.126.005-.141 0-.016-.006-.126-.137-.14-.141-.016-.005-.126.004-.141 0-.016-.004-.126.003-.141 0-.016-.003-.125.002-.14 0-.016-.002-.125.001-.141 0-.016-.002-.125 0-.141 0-.016-.001-.125-.14-.14-.141-.017 0-.125.14-.141.14h-.14c-.017.002-.126-.001-.141 0-.016.003-.125-.002-.141 0-.016.003-.125-.003-.14 0-.016.004-.126-.004-.141 0-.016.005-.126.136-.141.141-.015.005-.126-.005-.14 0-.015.006-.126-.006-.141 0-.015.007.014.134 0 .14-.015.008-.127-.006-.14 0-.015.008-.128-.007-.141 0-.014.009-.127.133-.141.141-.014.01-.128-.009-.14 0L65.5 12.834c-1.394-.7-2.82-1.276-4.36-1.828L59.735 2.71v-.14c-.003-.017.004-.125 0-.14-.004-.017.005-.125 0-.142-.004-.016-.136.016-.14 0-.005-.015.005-.124 0-.14-.006-.016.006-.125 0-.14-.007-.016.006-.126 0-.141-.007-.016-.134.015-.14 0-.008-.016.006-.126 0-.141-.009-.015.007-.126 0-.14-.009-.015-.133.014-.142 0-.008-.015.01-.127 0-.141-.008-.014-.13-.127-.14-.14-.01-.014.01-.128 0-.142-.01-.013-.13.013-.14 0-.011-.012.01-.128 0-.14-.012-.012-.13.012-.141 0-.012-.012.011-.129 0-.14-.013-.012-.129.01-.141 0-.012-.012.013-.13 0-.141-.013-.011-.128.01-.14 0-.014-.01.012-.13 0-.141-.014-.01-.128.01-.141 0-.014-.01-.127-.132-.14-.14-.015-.01-.127.008-.142 0-.014-.009-.125-.134-.14-.141-.015-.007-.125.006-.14 0-.016-.007.015-.135 0-.14-.016-.007-.126.005-.141 0-.016-.006-.125.004-.141 0-.016-.006-.125.004-.14 0-.017-.005-.125-.138-.141-.142-.016-.003-.125.004-.14 0-.017-.002-.125.003-.142 0-.016-.001-.124.002-.14 0H39.485a.436.436 0 0 0-.141 0c-.014.004-.127-.003-.14 0-.015.004-.128-.004-.142 0zm9.28 33.89c7.791 0 14.204 6.323 14.204 14.203 0 7.903-6.27 14.063-14.203 14.063-7.912 0-14.203-6.271-14.203-14.063 0-7.88 6.412-14.203 14.203-14.203z"/>
+ <path fill="#fff" d="M92.345 55.587v-16.5L82.22 37.4c-.75-2.719-1.875-5.344-3.188-7.688l5.907-8.343-11.625-11.72-8.344 5.907c-2.438-1.312-4.97-2.436-7.688-3.186L55.595 2.337h-16.5l-1.688 10.125c-2.718.75-5.343 1.875-7.687 3.188l-8.344-5.906L9.657 21.369l5.907 8.343c-1.313 2.438-2.438 4.97-3.188 7.782L2.345 39.087v16.5l10.031 1.688c.75 2.719 1.875 5.344 3.188 7.781L9.657 73.4l11.625 11.625 8.344-5.906c2.438 1.312 4.969 2.437 7.781 3.187l1.688 10.031h16.5l1.687-10.125c2.72-.75 5.344-1.875 7.688-3.187l8.344 5.906 11.625-11.625-5.907-8.344c1.313-2.437 2.438-4.968 3.188-7.78l10.125-1.595zm-45 9c-9.563 0-17.25-7.78-17.25-17.25 0-9.562 7.781-17.25 17.25-17.25 9.469 0 17.25 7.688 17.25 17.25 0 9.563-7.688 17.25-17.25 17.25z"/>
+</svg>
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/grayscale.svg b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/grayscale.svg
new file mode 100644
index 00000000..72bec238
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/grayscale.svg
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg">
+ <filter id="grayscale">
+ <feColorMatrix values="0.3333 0.3333 0.3333 0 0 0.3333 0.3333 0.3333 0 0 0.3333 0.3333 0.3333 0 0 0 0 0 1 0"/>
+ </filter>
+</svg>
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/icon_mmv.svg b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/icon_mmv.svg
new file mode 100644
index 00000000..067eaf14
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/icon_mmv.svg
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="58.4" height="52.3" viewBox="0 0 58.4 52.3">
+ <path fill="#36c" d="M0 0v44.7h58.4V0H0zm51.9 35.4H6l13.7-17.7 2-.8 12.9 13.7 6.8-4 10.5 8.8z"/>
+ <path fill="#fff" d="M48 4.2l1.9 1.9-5.1 5.1-1.3-1.3-.3 5.3 5.2-.3-1.3-1.3 5.2-5.1 1.8 1.9.4-6.6"/>
+ <path fill="#eaecf0" d="M0 44.7h58.4v7.7H0z"/>
+ <path fill="#36c" d="M1.9 46.2h13.8v1.6H1.9zm0 3.1h54.7v1.6H1.9z"/>
+</svg>
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/icon_page.svg b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/icon_page.svg
new file mode 100644
index 00000000..cdc570cc
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/icon_page.svg
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="58.4" height="52.3" viewBox="0 0 58.4 52.3">
+ <path fill="#eaecf0" d="M0 0h58.4v52.3H0z"/>
+ <path fill="#36c" d="M14.9 28.2h13.8v1.6H14.9zm0 3.1h38.8v1.6H14.9zm0 3h38.8v1.6H14.9zm0 5.2h13.8v1.6H14.9zm0 3h38.8v1.6H14.9zm0 3.1h38.8v1.6H14.9zm-.5-37.8v17.4h22.7V7.8H14.4zm20.2 13.8H16.7l5.3-6.9.8-.3 5 5.3 2.7-1.6 4.1 3.5z"/>
+ <path fill="#c8ccd1" d="M0 10.8h9.8v41.6H0z"/>
+ <circle cx="5.4" cy="4.8" r="3.7" fill="#c8ccd1"/>
+</svg>
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/license.svg b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/license.svg
new file mode 100644
index 00000000..97bfd989
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/license.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="5.5 -3.5 64 64">
+ <path fill="#72777d" d="M13 .5v56h38.5C57.45 56.5 62 51.95 62 46V.5H13zM51.5 53H20V4h14v28l7-7 7 7V4h10.5v42c0 3.85-3.15 7-7 7z"/>
+</svg>
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/link-hover.svg b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/link-hover.svg
new file mode 100644
index 00000000..60ae8395
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/link-hover.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="16.831" height="22.457" viewBox="0 0 16.831 22.457">
+ <path fill="#36c" d="M13.39.56a5.956 5.956 0 0 1 2.881 7.915l-1.521 3.262a6.024 6.024 0 0 1-.798 1.269c-.044.054-.089.104-.134.154l-.052.059a5.98 5.98 0 0 1-3.692 1.912l-.065.008a6.206 6.206 0 0 1-.821.035 5.94 5.94 0 0 1-4.291-2.002l.507-1.086a2.354 2.354 0 0 1 1.156-1.148l.025.045a3.14 3.14 0 0 0 1.37 1.233 3.136 3.136 0 0 0 1.876.248c.001.001.149-.012.384-.086.107-.034.23-.08.369-.146a3.136 3.136 0 0 0 1.561-1.541l.319-.683.192-.412.197-.422.956-2.051a3.153 3.153 0 0 0-5.715-2.665l-.839 1.798-.124.267a6.78 6.78 0 0 0-3.549 1.228c.016-.063.031-.127.049-.19.084-.29.192-.576.323-.858L5.476 3.44A5.954 5.954 0 0 1 13.39.56zM4.686 11.767l-.319.683-.192.412-.197.422-.956 2.051A3.153 3.153 0 0 0 8.737 18l.839-1.799.124-.267a6.79 6.79 0 0 0 3.549-1.228c-.016.063-.031.127-.049.19a5.95 5.95 0 0 1-.323.858l-1.521 3.262A5.956 5.956 0 0 1 .56 13.982l1.521-3.262c.217-.466.486-.89.798-1.269.044-.054.089-.104.133-.154l.051-.06a5.98 5.98 0 0 1 3.692-1.912c.022-.001.044-.005.065-.007a5.46 5.46 0 0 1 .387-.029 5.94 5.94 0 0 1 4.726 1.997l-.507 1.087a2.356 2.356 0 0 1-1.157 1.148 3.134 3.134 0 0 0-1.395-1.278 3.117 3.117 0 0 0-1.875-.248c-.001 0-.149.012-.384.087-.107.034-.23.079-.369.145a3.136 3.136 0 0 0-1.56 1.54z"/>
+</svg>
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/link.svg b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/link.svg
new file mode 100644
index 00000000..757b5983
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/link.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="16.831" height="22.457" viewBox="0 0 16.831 22.457">
+ <path fill="#72777d" d="M13.39.56a5.956 5.956 0 0 1 2.881 7.915l-1.521 3.262a6.024 6.024 0 0 1-.798 1.269c-.044.054-.089.104-.134.154l-.052.059a5.98 5.98 0 0 1-3.692 1.912l-.065.008a6.206 6.206 0 0 1-.821.035 5.94 5.94 0 0 1-4.291-2.002l.507-1.086a2.354 2.354 0 0 1 1.156-1.148l.025.045a3.14 3.14 0 0 0 1.37 1.233 3.136 3.136 0 0 0 1.876.248c.001.001.149-.012.384-.086.107-.034.23-.08.369-.146a3.136 3.136 0 0 0 1.561-1.541l.319-.683.192-.412.197-.422.956-2.051a3.153 3.153 0 0 0-5.715-2.665l-.839 1.798-.124.267a6.78 6.78 0 0 0-3.549 1.228c.016-.063.031-.127.049-.19.084-.29.192-.576.323-.858L5.476 3.44A5.954 5.954 0 0 1 13.39.56zM4.686 11.767l-.319.683-.192.412-.197.422-.956 2.051A3.153 3.153 0 0 0 8.737 18l.839-1.799.124-.267a6.79 6.79 0 0 0 3.549-1.228c-.016.063-.031.127-.049.19a5.95 5.95 0 0 1-.323.858l-1.521 3.262A5.956 5.956 0 0 1 .56 13.982l1.521-3.262c.217-.466.486-.89.798-1.269.044-.054.089-.104.133-.154l.051-.06a5.98 5.98 0 0 1 3.692-1.912c.022-.001.044-.005.065-.007a5.46 5.46 0 0 1 .387-.029 5.94 5.94 0 0 1 4.726 1.997l-.507 1.087a2.356 2.356 0 0 1-1.157 1.148 3.134 3.134 0 0 0-1.395-1.278 3.117 3.117 0 0 0-1.875-.248c-.001 0-.149.012-.384.087-.107.034-.23.079-.369.145a3.136 3.136 0 0 0-1.56 1.54z"/>
+</svg>
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/location.svg b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/location.svg
new file mode 100644
index 00000000..92d1a83e
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/location.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="80" height="80" viewBox="0 0 80 80">
+ <path fill="#72777d" d="M61.615 10.966A30.392 30.392 0 0 0 51.506 4.25a30.615 30.615 0 0 0-33.178 6.728c-5.783 5.792-8.975 13.166-8.975 20.791 0 7.613 3.19 14.995 8.975 20.781l3.182 3.153c6.386 6.31 11.893 11.758 16.618 19.331L39.966 78l1.85-2.966c4.725-7.573 10.232-13.021 16.607-19.324l3.191-3.17c12.044-12.044 12.044-29.529.001-41.574zM49.477 42.641c-5.25 5.258-13.76 5.258-19.011 0-5.249-5.24-5.249-13.75 0-19 5.251-5.24 13.761-5.24 19.011 0 5.238 5.25 5.238 13.758 0 19z"/>
+</svg>
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/mw-close.svg b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/mw-close.svg
new file mode 100644
index 00000000..6e02593f
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/mw-close.svg
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="23" height="23" viewBox="0 0 23 23">
+ <path fill-opacity=".51" d="M19.65 0a.436.436 0 0 0-.282.125l-8.031 8L3.493.282C3.336.125 3.103.109 2.962.25L.274 2.938c-.14.141-.125.375.032.531l7.843 7.844-8.021 8.021c-.157.157-.172.422-.031.563l2.687 2.656c.141.141.375.126.532-.031l8.02-8.021 8.21 8.208c.156.157.39.173.531.032l2.688-2.688c.14-.14.125-.374-.032-.531l-8.209-8.209 8.032-8.031c.156-.157.172-.39.03-.531L19.9.094a.311.311 0 0 0-.25-.093z"/>
+ <path fill="#fff" d="M19.65.657a.32.32 0 0 0-.22.093l-8.093 8.094L3.43.938a.316.316 0 0 0-.438 0L1.306 2.625a.316.316 0 0 0 0 .438l7.906 7.906-8.094 8.094a.316.316 0 0 0 0 .437l1.719 1.688a.278.278 0 0 0 .406 0l8.094-8.094 8.281 8.281c.118.118.32.118.438 0l1.687-1.718a.278.278 0 0 0 0-.407l-8.281-8.28 8.094-8.095a.316.316 0 0 0 0-.437L19.868.75a.32.32 0 0 0-.219-.093z"/>
+</svg>
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/mw-defullscreen-ltr.svg b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/mw-defullscreen-ltr.svg
new file mode 100644
index 00000000..6d0936af
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/mw-defullscreen-ltr.svg
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="28.391" height="29.067">
+ <path fill-opacity=".51" d="M15.224 0c-.178 0-.313.083-.313.188v13.437c0 .2.086.375.188.375h12.625c.1 0 .187-.176.187-.375V9.844c0-.2-.086-.344-.187-.344h-5.22l5.845-5.844c.08-.08.047-.265-.094-.406L25.567.562c-.14-.14-.326-.173-.406-.093l-6.25 6.25V.187c0-.104-.135-.187-.312-.187h-3.375z"/>
+ <path fill="#fff" d="M15.724.5c-.167 0-.313.047-.313.125v12.219c0 .01.026.02.031.031a.257.257 0 0 0 .031.062c.012.01.017.025.032.032.005.004.025-.003.03 0 .01.005.022.03.032.03h11.719c.075 0 .125-.145.125-.312v-2.375c0-.166-.05-.312-.125-.312h-6.688l6.719-6.75c.06-.06.024-.195-.094-.313L25.536 1.25c-.118-.118-.284-.185-.344-.125l-6.781 6.812V.625c0-.079-.115-.125-.281-.125h-2.407z"/>
+ <path fill-opacity=".51" d="M.974 15c-.102 0-.188.144-.188.344v3.781c0 .2.086.375.188.375H6.38L.036 25.844c-.082.082-.016.265.125.406l2.656 2.656c.141.141.324.208.407.125l6.562-6.562v6.312c0 .11.135.219.313.219h3.343c.178 0 .344-.11.344-.219V15.188c0-.034-.038-.067-.062-.094a.229.229 0 0 0-.094-.063c-.013-.006-.017-.031-.031-.031H.974z"/>
+ <path fill="#fff" d="M1.411 15.5c-.075 0-.125.115-.125.281v2.406c0 .167.05.313.125.313h6.656l-7 7.031c-.06.06-.024.195.094.313L2.88 27.53c.117.118.252.185.312.125l7.094-7.125v7.313c0 .078.115.156.281.156h2.406c.167 0 .313-.078.313-.156V15.625c0-.026-.037-.042-.063-.063-.009-.02-.019-.017-.03-.03-.009-.005-.024.002-.032 0-.013-.01-.017-.032-.031-.032H1.41z"/>
+</svg>
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/mw-defullscreen-rtl.svg b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/mw-defullscreen-rtl.svg
new file mode 100644
index 00000000..16266e47
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/mw-defullscreen-rtl.svg
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="28.391" height="29.067">
+ <path fill-opacity=".51" d="M13.168 0c.177 0 .312.083.312.188v13.437c0 .2-.086.375-.187.375H.668c-.101 0-.188-.176-.188-.375V9.844c0-.2.087-.344.188-.344h5.218L.043 3.656c-.08-.08-.048-.265.093-.406L2.824.562c.14-.14.326-.173.406-.093l6.25 6.25V.188c0-.105.135-.188.313-.188h3.375z"/>
+ <path fill="#fff" d="M12.668.5c.166 0 .312.047.312.125v12.219c0 .01-.026.02-.031.031a.257.257 0 0 1-.031.062c-.012.01-.016.025-.031.032-.005.004-.026-.003-.032 0-.01.005-.02.03-.03.03H1.104c-.075 0-.125-.145-.125-.312v-2.375c0-.166.05-.312.125-.312h6.688L1.074 3.25c-.06-.06-.024-.195.094-.313L2.855 1.25c.118-.118.284-.185.344-.125L9.98 7.937V.625c0-.079.115-.125.282-.125h2.406z"/>
+ <path fill-opacity=".51" d="M27.418 15c.1 0 .187.144.187.344v3.781c0 .2-.086.375-.187.375H22.01l6.344 6.344c.083.082.016.265-.125.406l-2.656 2.656c-.141.141-.324.208-.406.125l-6.563-6.562v6.312c0 .11-.135.219-.312.219h-3.344c-.177 0-.344-.11-.344-.219V15.188c0-.034.039-.067.063-.094a.23.23 0 0 1 .093-.063c.014-.006.017-.03.032-.03h12.625z"/>
+ <path fill="#fff" d="M26.98 15.5c.075 0 .125.115.125.281v2.406c0 .167-.05.313-.125.313h-6.656l7 7.031c.06.06.024.195-.094.313l-1.719 1.687c-.117.118-.252.185-.312.125l-7.094-7.125v7.313c0 .078-.115.156-.281.156h-2.406c-.166 0-.313-.078-.313-.156V15.625c0-.026.037-.042.063-.063.009-.02.02-.017.03-.03.009-.005.024.002.032 0 .013-.01.017-.032.031-.032h11.72z"/>
+</svg>
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/mw-download.svg b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/mw-download.svg
new file mode 100644
index 00000000..5603c753
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/mw-download.svg
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
+ <path fill-opacity=".51" d="M9.944.006a1.62 1.62 0 0 0-1.157.469 1.62 1.62 0 0 0-.468 1.156v7.875h-4s.006.025 0 .031c-.006.007-.031 0-.032 0 0 0 .005.029 0 .032-.004.003-.03 0-.03 0-.004.003-.028-.004-.032 0h-.031V9.6c-.004.003-.028-.004-.032 0 0 0 .008.028 0 .031-.007.004-.03 0-.03 0s-.005.025 0 .031c-.012-.005-.032 0-.032 0h-.031s-.004.025 0 .032h-.032s-.004.024 0 .031c0 0 .005.029 0 .031l-.03.031s-.028-.003-.032 0v.032s.027.003.031 0l-.031.031c-.006-.004.004-.028 0-.031-.007-.005-.031 0-.031 0a.097.097 0 0 0 0 .031v.031c-.004.004.004.028 0 .031.004.003-.006.028 0 .032-.011-.005-.032 0-.032 0v.125a2.53 2.53 0 0 0 0 .062v.188s.02.005.032 0c-.006.004.004.028 0 .031v.094s.024.004.031 0c.004-.003-.006-.028 0-.032.009-.005.022.006.031 0-.006.005.005.029 0 .032-.006.004-.031 0-.031 0v.031h.031c.004.003-.004.028 0 .031.004.008.032 0 .031 0l.032-.031c.006-.006.031 0 .031 0s.011.037 0 .063c-.002.004-.03-.004-.031 0-.005.006 0 .03 0 .03H4.1l6.031 7.438H3.069a.097.097 0 0 0-.032 0h-.062v.032c-.004.003-.028-.004-.031 0 0 0 .006.026 0 .03-.006.005-.031 0-.032 0H2.85v.032s.002.027 0 .031-.031 0-.031 0h-.032v.032s.006.023 0 .03c-.005.009-.03 0-.03 0s.003.03 0 .032c-.004.002-.032 0-.032 0h-.031v.031h-.032v.062s.005.025 0 .032c-.004.007-.03 0-.03 0v.063s.003.023 0 .03c-.005.009-.032 0-.032 0v2.97a.705.705 0 0 0 0 .062v.125h.031v.063s.024-.01.031 0c.008.01 0 .03 0 .03v.063s.024-.008.032 0c.007.008 0 .031 0 .032h.031c.003.003-.004.027 0 .03.004.004.029-.002.031 0 .001.002-.007.025 0 .032.008.007.04.009.031 0-.008-.008-.007.024 0 .031.008.008.03 0 .032 0 .002.003-.004.028 0 .032.003.003.028-.004.031 0a.097.097 0 0 0 0 .03c.008.008.031 0 .031 0 .008.008 0 .032 0 .032h.031c.004.006.032 0 .032 0h.031c.01.008 0 .031 0 .031h.062v.032h.125a1.698 1.698 0 0 0 .063 0h15.594a.704.704 0 0 0 .062 0h.032a.097.097 0 0 0 .03 0h.063v-.032h.062s-.009-.023 0-.031c0 0 .025.006.032 0 0 0 .028.006.031 0h.032s-.008-.024 0-.031c.008-.008.03 0 .03 0 .008-.008 0-.032 0-.031.004-.004.028.003.032 0 .004-.004-.003-.03 0-.032.001 0 .024.008.031 0 .008-.007.009-.04 0-.031-.008.009.024.007.032 0 .007-.007-.001-.03 0-.031.002-.003.027.003.03 0 .004-.004-.003-.028 0-.031l.032-.032c.008-.008.031 0 .031 0v-.031a.097.097 0 0 0 0-.031c.006-.007 0-.032 0-.031.008-.01.031 0 .032 0V21.6h.03v-.062a.097.097 0 0 0 0-.032v-.031a1.698 1.698 0 0 0 0-.063v-2.78a.704.704 0 0 0 0-.063v-.125h-.03c-.005-.004.003-.028 0-.031v-.032s-.024.01-.032 0c0 0 .006-.024 0-.031v-.062s-.023.008-.031 0l-.031-.032c-.004-.003.003-.027 0-.031-.004-.004-.029.003-.031 0-.001-.001.007-.024 0-.031-.008-.008-.04-.009-.032 0 .009.008.008-.024 0-.031-.007-.008-.03 0-.031 0-.003-.003.004-.028 0-.032-.004-.003-.028.004-.031 0V18.1c-.008-.008-.032 0-.031 0-.008-.008 0-.031 0-.031h-.032c-.003-.006-.031 0-.031 0-.007-.006-.031 0-.031 0-.01-.008 0-.031 0-.031h-.063v-.032h-.125a1.698 1.698 0 0 0-.062 0H11.1v-.031l6.625-7.406h.063s.004-.025 0-.031c-.005-.003.005-.028 0-.032l-.032.032s-.012-.043-.031-.063c.005.002.019-.006.031 0 .013.006.02-.005.032 0 .004.001-.004.03 0 .031.011.004.03 0 .03 0 .005-.003-.003-.028 0-.031 0 0 .027.003.032 0v-.031s-.025.004-.031 0l.031-.031v.03c.006.005.031 0 .031 0a.097.097 0 0 0 0-.03v-.094c.011.005.032 0 .032 0v-.188a2.81 2.81 0 0 0 0-.062v-.125s-.02-.005-.032 0v-.031c-.003-.004.004-.028 0-.032v-.062s-.024-.005-.031 0c-.004.003.006.027 0 .031-.023.016-.063.031-.063.031.006-.007-.007-.022 0-.031.008-.014.024-.017.032-.031.006-.005.031 0 .031 0v-.031h-.031c-.004-.004.004-.028 0-.032 0 0-.02-.005-.031 0v-.031s-.021-.006-.032 0c.005-.007 0-.031 0-.031v-.031s-.024-.005-.031 0V9.63s-.025-.004-.031 0c0 0-.025-.006-.031 0-.003-.002.003-.027 0-.031h-.032v-.031a.097.097 0 0 0-.031 0 .12.12 0 0 0-.031 0s-.027.003-.031 0v-.031c-.006-.007-.032 0-.032 0v-.032h-.093a.097.097 0 0 0-.032 0H13.57L13.757.63a.704.704 0 0 0 0-.062V.444h-.031c-.004-.004.004-.028 0-.031V.38s-.024.01-.031 0V.288s-.024.007-.031 0c0 0 .007-.024 0-.032a.097.097 0 0 0-.032 0c-.003-.003.004-.027 0-.031-.003-.004-.028.003-.031 0 0-.001.007-.024 0-.031-.007-.008-.04-.009-.031 0 .008.008.007-.024 0-.031-.008-.008-.03 0-.031 0-.003-.003.003-.028 0-.032-.004-.003-.028.004-.032 0a.097.097 0 0 0 0-.031s-.023.008-.031 0 0-.031 0-.031h-.031c-.003-.006-.031 0-.031 0s-.022.007-.032 0c-.01-.008 0-.031 0-.031h-.062V.006h-.125a1.693 1.693 0 0 0-.063 0H9.944zM4.069 10.537l-.032-.03c.005.007 0 .03 0 .03s.025.007.032 0zm.093-.875c.004.012-.004.019 0 .032-.008-.009-.024.006-.03 0l.03-.032zm13.594.094c.009-.004.023.035.032.031-.01.006-.023-.005-.032 0 .005-.002-.006-.027 0-.03zm-13.75.094c.009.006.023-.006.032 0 .006.007 0 .031 0 .031s-.017-.021-.032-.031z"/>
+ <path fill="#fff" d="M9.71.435c-.6 0-1 .4-1 1v8.5H4.305l6.094 7.5 6.688-7.5H12.71l.187-9.5H9.711zm-6.718 18v2.78h15.594v-2.78H2.992z"/>
+</svg>
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/mw-fullscreen-ltr.svg b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/mw-fullscreen-ltr.svg
new file mode 100644
index 00000000..64850f61
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/mw-fullscreen-ltr.svg
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="21.125" height="21.781">
+ <path fill-opacity=".51" d="M8.313 0c-.102 0-.188.144-.188.344v3.781c0 .2.086.375.188.375h5.124L4 13.938v-5.97c0-.104-.135-.187-.313-.187H.313c-.178 0-.313.083-.313.188v13.437c0 .2.086.375.188.375h12.624c.102 0 .188-.175.188-.375v-3.781c0-.2-.086-.344-.188-.344H7L17.125 7.188v6.593c0 .11.166.219.344.219h3.343c.178 0 .313-.11.313-.219V.187c0-.012-.027-.019-.031-.03-.005-.016.01-.05 0-.063-.019-.037-.038-.045-.063-.063-.008-.005-.022.005-.031 0C20.987.025 20.983 0 20.969 0H8.312z"/>
+ <path fill="#fff" d="M8.781.5c-.075 0-.156.115-.156.281v2.406c0 .167.081.313.156.313h6.375L3.5 15.156v-6.75c0-.078-.146-.125-.313-.125H.781c-.166 0-.281.047-.281.125v12.219c0 .013.024.02.031.031.003.008-.003.024 0 .031.015.017.036.019.063.032.016.015.012.062.03.062h11.75c.076 0 .126-.146.126-.312v-2.407c0-.166-.05-.28-.125-.28H5.094L17.624 5.25v7.594c0 .078.147.156.313.156h2.407c.166 0 .28-.078.28-.156V.624c0-.014-.021-.017-.03-.03-.005-.008.008-.024 0-.032-.01-.02-.02-.017-.031-.03-.008-.005-.023.002-.032 0-.013-.01-.016-.032-.03-.032H8.78z"/>
+</svg>
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/mw-fullscreen-rtl.svg b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/mw-fullscreen-rtl.svg
new file mode 100644
index 00000000..13c751d0
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/mw-fullscreen-rtl.svg
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="21.125" height="21.781">
+ <path fill-opacity=".51" d="M12.813 0c.1 0 .187.144.187.344v3.781c0 .2-.086.375-.188.375H7.689l9.437 9.438v-5.97c0-.104.135-.187.313-.187h3.375c.177 0 .312.083.312.188v13.437c0 .2-.086.375-.188.375H8.313c-.1 0-.187-.175-.187-.375v-3.781c0-.2.086-.344.188-.344h5.812L4 7.188v6.593c0 .11-.166.219-.344.219H.312C.136 14 0 13.89 0 13.781V.187C0 .175.028.169.031.158.036.14.021.106.031.093.05.057.07.049.094.03c.008-.005.022.004.03 0C.139.025.143.001.157.001h12.656z"/>
+ <path fill="#fff" d="M12.344.5c.075 0 .156.115.156.281v2.406c0 .167-.081.313-.156.313H5.969l11.656 11.656v-6.75c0-.078.146-.125.313-.125h2.406c.166 0 .281.047.281.125v12.219c0 .013-.024.02-.031.031-.003.008.003.024 0 .031-.015.017-.036.019-.063.032-.016.015-.012.062-.03.062H8.75c-.076 0-.126-.146-.126-.312v-2.407c0-.166.05-.28.125-.28h7.281L3.501 5.25v7.594c0 .078-.147.156-.313.156H.78c-.166 0-.28-.078-.28-.156V.624C.5.61.521.608.53.595.536.586.523.57.531.562c.01-.02.02-.017.031-.03.008-.005.023.002.032 0C.607.521.61.5.624.5h11.72z"/>
+</svg>
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/next-ltr.svg b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/next-ltr.svg
new file mode 100644
index 00000000..c8ab0d1c
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/next-ltr.svg
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="26" height="40" viewBox="0 0 26 40">
+ <path fill-opacity=".51" d="M6 .007a.403.403 0 0 0-.281.125L.656 5.226a.395.395 0 0 0 0 .563l14.188 14.218L.656 34.227a.395.395 0 0 0 0 .562l5.063 5.093a.434.434 0 0 0 .593 0l14.47-14.5 5.093-5.093a.403.403 0 0 0 .125-.282.433.433 0 0 0-.031-.156.392.392 0 0 0-.094-.125l-5.094-5.094L6.313.132A.478.478 0 0 0 6 .007z"/>
+ <path fill="#fff" d="M6 .695c-.077 0-.129.004-.188.062L1.97 4.601a.278.278 0 0 0 0 .406L16.28 19.29 1.344 34.226a.395.395 0 0 0 0 .563l3.687 3.656a.395.395 0 0 0 .563 0L20.719 23.32l3.75-3.75c.022-.022.046-.037.062-.063.017-.017.02-.042.031-.062.004-.009-.003-.022 0-.031.039-.08.036-.139 0-.219-.008-.02.013-.045 0-.062-.005-.014-.024-.019-.03-.032l-.063-.093-3.688-3.657L6.22.758C6.159.698 6.077.695 6 .695z"/>
+</svg>
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/next-rtl.svg b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/next-rtl.svg
new file mode 100644
index 00000000..e27085ec
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/next-rtl.svg
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="26" height="40" viewBox="0 0 26 40">
+ <path fill-opacity=".51" d="M20 .007c.102 0 .203.047.281.125l5.063 5.094a.395.395 0 0 1 0 .563L11.156 20.007l14.188 14.22a.395.395 0 0 1 0 .562l-5.063 5.093a.434.434 0 0 1-.593 0l-14.47-14.5L.126 20.29A.403.403 0 0 1 0 20.007c0-.05.012-.108.031-.156a.392.392 0 0 1 .094-.125l5.094-5.094L19.687.132A.478.478 0 0 1 20 .007z"/>
+ <path fill="#fff" d="M20 .695c.077 0 .129.004.188.062l3.843 3.844a.278.278 0 0 1 0 .406L9.72 19.29l14.937 14.937a.395.395 0 0 1 0 .563l-3.687 3.656a.395.395 0 0 1-.563 0L5.281 23.32l-3.75-3.75c-.022-.022-.046-.037-.062-.063-.017-.017-.02-.042-.031-.062-.004-.009.003-.022 0-.031-.039-.08-.036-.139 0-.219.008-.02-.013-.045 0-.062.005-.014.024-.019.03-.032l.063-.093L5.22 15.35 19.78.758c.06-.06.142-.063.219-.063z"/>
+</svg>
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/open.svg b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/open.svg
new file mode 100644
index 00000000..559f8faf
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/open.svg
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="768">
+ <path fill="#72777d" d="M304 179.2v355.2h464V179.2H304zm412.8 281.6H352L460.8 320l16-6.4 102.4 108.8 54.4-32 83.2 70.4z"/>
+ <path fill="#72777d" d="M256 230.4v358.4h499.2v-25.6H281.6V230.4"/>
+</svg>
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/page.svg b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/page.svg
new file mode 100644
index 00000000..97b10d33
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/page.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="212 84 600 600.8">
+ <path fill="#fff" d="M422.4 233.6h180v60h-180v-60zm299.2 120v60H422.4v-60h299.2zM422.4 534.4v-60h300v60h-300zM332 233.6h-84l-5.6-8v-67.2c0-24 21.6-44 45.6-44s44 20 44 44v75.2zM812 148c0-35.2-60-64-60-64H276c-35.2 0-64 28.8-64 64v100l16 16h104v420.8h480V148z"/>
+</svg>
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/pasting-hover.svg b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/pasting-hover.svg
new file mode 100644
index 00000000..6e9c277d
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/pasting-hover.svg
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="18.244" height="22.457" viewBox="0 0 18.244 22.457">
+ <g fill="#36c" fill-rule="evenodd">
+ <path d="M2.773 1.936C1.21 2.237 0 3.612 0 5.256v13.799c0 1.86 1.54 3.402 3.4 3.402h11.444c1.86 0 3.4-1.542 3.4-3.402v-13.8c0-1.643-1.21-3.018-2.773-3.32.129.332.207.687.207 1.06v.175c0 .591-.184 1.145-.49 1.613a.555.555 0 0 1 .253.473v13.799c0 .355-.241.597-.597.597H3.4c-.355 0-.597-.242-.597-.597v-13.8c0-.214.105-.369.254-.472a2.938 2.938 0 0 1-.49-1.613v-.176c0-.372.078-.728.206-1.058z"/>
+ <path d="M8.096 0c-.831 0-1.5.669-1.5 1.5v.004h-1.04c-.824 0-1.49.665-1.49 1.49v.176c0 .825.666 1.488 1.49 1.488h7.133c.825 0 1.489-.663 1.489-1.488v-.176c0-.825-.664-1.49-1.489-1.49H11.65V1.5c0-.831-.669-1.5-1.5-1.5z"/>
+ </g>
+</svg>
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/pasting.svg b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/pasting.svg
new file mode 100644
index 00000000..70ef8de0
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/pasting.svg
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="18.244" height="22.457" viewBox="0 0 18.244 22.457">
+ <g fill="#72777d" fill-rule="evenodd">
+ <path d="M2.773 1.936C1.21 2.237 0 3.612 0 5.256v13.799c0 1.86 1.54 3.402 3.4 3.402h11.444c1.86 0 3.4-1.542 3.4-3.402v-13.8c0-1.643-1.21-3.018-2.773-3.32.129.332.207.687.207 1.06v.175c0 .591-.184 1.145-.49 1.613a.555.555 0 0 1 .253.473v13.799c0 .355-.241.597-.597.597H3.4c-.355 0-.597-.242-.597-.597v-13.8c0-.214.105-.369.254-.472a2.938 2.938 0 0 1-.49-1.613v-.176c0-.372.078-.728.206-1.058z"/>
+ <path d="M8.096 0c-.831 0-1.5.669-1.5 1.5v.004h-1.04c-.824 0-1.49.665-1.49 1.49v.176c0 .825.666 1.488 1.49 1.488h7.133c.825 0 1.489-.663 1.489-1.488v-.176c0-.825-.664-1.49-1.489-1.49H11.65V1.5c0-.831-.669-1.5-1.5-1.5z"/>
+ </g>
+</svg>
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/pd.svg b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/pd.svg
new file mode 100644
index 00000000..c98d1b6c
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/pd.svg
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="5.5 -3.5 64 64">
+ <path fill="#72777d" d="M54.414 39.014c.915-1.828 1.372-4.114 1.372-5.943h-7.772l6.4 5.943zM53.5 17.071C48.471 7.93 35.671 5.186 26.986 11.586l5.485 5.485c5.943-4.571 14.172 0 14.629 6.858h8.229c0-2.743-.915-5.943-1.829-6.858-.914-1.828.914 1.829 0 0z"/>
+ <path fill="#72777d" d="M37.5-3.5c-17.829 0-32 14.171-32 32s14.171 32 32 32 32-14.171 32-32-14.171-32-32-32zm-27.429 32c0-5.486 1.829-10.514 4.572-14.629l6.4 6.4c-2.743 7.772-1.829 17.372 4.114 23.772 5.486 5.486 15.086 6.857 22.4 3.2l4.572 5.028c-4.115 2.743-9.143 4.115-14.172 4.115-15.543-.457-27.886-12.8-27.886-27.886zm17.829-.914l13.714 14.171c-4.114 1.372-8.685 0-11.428-3.657-1.829-3.2-2.286-7.314-2.286-10.514zM57.157 47.7l-38.4-38.857C23.33 3.814 30.186 1.07 37.5 1.07c15.086 0 27.429 12.343 27.429 27.429 0 7.314-3.2 14.171-7.772 19.2z"/>
+</svg>
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/prev-ltr.svg b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/prev-ltr.svg
new file mode 100644
index 00000000..e27085ec
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/prev-ltr.svg
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="26" height="40" viewBox="0 0 26 40">
+ <path fill-opacity=".51" d="M20 .007c.102 0 .203.047.281.125l5.063 5.094a.395.395 0 0 1 0 .563L11.156 20.007l14.188 14.22a.395.395 0 0 1 0 .562l-5.063 5.093a.434.434 0 0 1-.593 0l-14.47-14.5L.126 20.29A.403.403 0 0 1 0 20.007c0-.05.012-.108.031-.156a.392.392 0 0 1 .094-.125l5.094-5.094L19.687.132A.478.478 0 0 1 20 .007z"/>
+ <path fill="#fff" d="M20 .695c.077 0 .129.004.188.062l3.843 3.844a.278.278 0 0 1 0 .406L9.72 19.29l14.937 14.937a.395.395 0 0 1 0 .563l-3.687 3.656a.395.395 0 0 1-.563 0L5.281 23.32l-3.75-3.75c-.022-.022-.046-.037-.062-.063-.017-.017-.02-.042-.031-.062-.004-.009.003-.022 0-.031-.039-.08-.036-.139 0-.219.008-.02-.013-.045 0-.062.005-.014.024-.019.03-.032l.063-.093L5.22 15.35 19.78.758c.06-.06.142-.063.219-.063z"/>
+</svg>
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/prev-rtl.svg b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/prev-rtl.svg
new file mode 100644
index 00000000..c8ab0d1c
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/prev-rtl.svg
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="26" height="40" viewBox="0 0 26 40">
+ <path fill-opacity=".51" d="M6 .007a.403.403 0 0 0-.281.125L.656 5.226a.395.395 0 0 0 0 .563l14.188 14.218L.656 34.227a.395.395 0 0 0 0 .562l5.063 5.093a.434.434 0 0 0 .593 0l14.47-14.5 5.093-5.093a.403.403 0 0 0 .125-.282.433.433 0 0 0-.031-.156.392.392 0 0 0-.094-.125l-5.094-5.094L6.313.132A.478.478 0 0 0 6 .007z"/>
+ <path fill="#fff" d="M6 .695c-.077 0-.129.004-.188.062L1.97 4.601a.278.278 0 0 0 0 .406L16.28 19.29 1.344 34.226a.395.395 0 0 0 0 .563l3.687 3.656a.395.395 0 0 0 .563 0L20.719 23.32l3.75-3.75c.022-.022.046-.037.062-.063.017-.017.02-.042.031-.062.004-.009-.003-.022 0-.031.039-.08.036-.139 0-.219-.008-.02.013-.045 0-.062-.005-.014-.024-.019-.03-.032l-.063-.093-3.688-3.657L6.22.758C6.159.698 6.077.695 6 .695z"/>
+</svg>
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/restrict-2257.svg b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/restrict-2257.svg
new file mode 100644
index 00000000..920f2f54
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/restrict-2257.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
+ <path fill="#222" d="M64 63.43H45.256L31.704 42.744 17.9 63.43H-8l30.29-31.662L.464.57h18.702l13.087 19.588L45.72.57h17.942L41.71 31.135 64 63.43z"/>
+</svg>
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/restrict-aus-reserve.svg b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/restrict-aus-reserve.svg
new file mode 100644
index 00000000..bb5c53f8
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/restrict-aus-reserve.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
+ <path fill="#222" d="M49.024 2.346c-2.179 2.268-2.006 5.62-2.19 8.515-.247 3.166-3.671 4.733-5.458 1.819-2.273-1.17-7.088-3.087-3.768-6.01 1.239-1.461 1.625-2.656-.662-2.304-2.317.615-5.01-2.55-6.605-1.01.67 1.932-3.56.545-3.975 3.091-.499 2.203-1.535 4.134-3.75 2.016-2.504-2.237-4.987 1.97-5.734 2.799.853 1.324-2.909 1.143-.965 2.49-.833 2.001-1.932-3.072-2.565-.021.343 3.524-2.84 6.01-6.048 6.818-2.81.252-4.573 3.223-6.714 3.87-.87-1.04-.65 4.011-.156 5.458C2.787 31.029.2 34.153 2.64 35.504c1.184 2.587 3.321 5.234 3.168 8.13-2.186 3.172 3.68 4.56 5.343 2.11 1.913-2.355 5.297-.565 7.32-2.559 2.039-2.874 6.43-2.899 9.66-3.857 3.368-.274 5.443 1.8 7.18 4.096-.43 2.596 1.588 1.655 2.096-.137.903-1.066 2.574-1.662 1.27.226-.998.99-.486 3.618.272 1.107 2.06-.86-.1 2.63 2.359 2.515-.024 2.905 2.293 4.83 5.199 5.168 2.456-.574 4.634 1.692 6.822-.36 4.33.004 3.583-5.22 6.362-7.349 3.678-3.691 4.851-9.228 4.09-14.26-1.857-1.748-2.183-5.43-4.382-6.134-.06-2.62-2.088-4.984-4.671-6.257-.734-2.389-1.338-5.474-1.686-8.096-4.132-.394-1.933-4.892-4.017-7.5zm-.4 54.03c-.893 2.043.675 7.189 3.696 4.534 1.1-1.99 1.28-5.248-1.923-3.8-.63-.162-1.114-.655-1.773-.734z"/>
+</svg>
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/restrict-communist.svg b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/restrict-communist.svg
new file mode 100644
index 00000000..65950c16
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/restrict-communist.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
+ <path fill="#222" fill-rule="evenodd" d="M31.934 0C55.19 15.901 56.025 29.211 53.04 40.015c-.646 2.34-1.731 4.446-3.252 6.19-1.29 1.479-2.938 2.724-4.996 3.683-6.357 2.963-16.17 1.966-29.08-5.537l-2.441 3.147c-.516-.385-1.798-.86-2.783.345-.953 1.166.395 2.498-.1 3.362-.44.767-.583.747-3.388 2.48-5.579 3.445-6.784 8.087-5.137 9.709 1.607 1.582 6.333-.348 9.027-6.834 1.14-2.744.882-2.379 1.752-3.31.734-.787 2.084.31 2.951-.67.92-1.039.372-2.533-.004-3.043l.793-.91c13.014 11.687 26.154 13.391 35.115 8.478l6.147 6.623c.64.664 2.239.07 3.584-1.326 1.344-1.397 1.91-3.057 1.271-3.721l-5.793-5.687c3.32-3.655 5.47-8.657 5.975-14.64C64.472 17.128 48.11 4.708 31.934 0zM22.95 11.512L9.497 25.488l5.82 6.043 6.326-6.57 3.164-3.29 7.996-8.306-9.853-1.853zM38.99 19.5c-.872 0-1.578.276-2.12.83-.543.554-.813 1.286-.813 2.193 0 1.002.276 2.659.83 4.969l.92 3.853c.46 1.91.73 3.537.812 4.881h.78c.153-1.886.394-3.513.724-4.88l.938-3.854c.59-2.452.883-4.126.883-5.022 0-.86-.282-1.567-.848-2.12-.554-.566-1.257-.85-2.105-.85zm.038 19.253c-.802 0-1.48.284-2.034.85a2.804 2.804 0 0 0-.832 2.033c0 .79.278 1.468.832 2.033.566.554 1.244.83 2.034.83.79 0 1.461-.276 2.015-.83a2.773 2.773 0 0 0 .848-2.033c0-.801-.282-1.479-.848-2.033a2.717 2.717 0 0 0-2.015-.85z"/>
+</svg>
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/restrict-costume.svg b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/restrict-costume.svg
new file mode 100644
index 00000000..b8f3447a
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/restrict-costume.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
+ <path fill="#222" fill-rule="evenodd" d="M50.477 0c-6.606 4.229-11.73 10.863-19.481 13.187-6.862 2.179-14.184 1.266-21.231.839-3.598-1.417-6.256 1.87-4.78 5.25 1.647 9.517 6.173 18.306 11.46 26.296 4.395 6.31 9.82 12.151 16.6 15.906 5.76 2.912 13.311 3.862 18.784-.206 4.091-2.592 5.26-7.596 6.422-11.969 2.205-9.96.865-20.24-.353-30.234-.829-5.258-2.991-10.171-4.861-15.118-.728-1.345-1.2-3.102-2.56-3.951zm-6.295 13.741c3.165.799 11.212 2.413 6.02 5.54l-13.199 4.313c1.264-5.245.32-11.22 7.18-9.853zm-22.347 8.716c3.165.799 11.212 2.413 6.02 5.54l-13.2 4.312c1.264-5.245.321-11.22 7.18-9.852zM51.762 33.39c5.181 3.66 1.884 9.062.853 12.706-1.71 4.281-11.064 9.595-15.575 7.469-4.524-1.543-7.244-2.464-11.86-6.9-.629-6.497 3.78-3.115 7.055-2.573 5.357 1.752 12.296.678 14.994-4.814 1.566-2.515 1.583-4.709 4.533-5.888z"/>
+</svg>
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/restrict-currency.svg b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/restrict-currency.svg
new file mode 100644
index 00000000..82aa8e44
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/restrict-currency.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
+ <path fill="#222" d="M30.69.318c-6.128-.134-11.905 3.47-14.575 8.834l-3.205-.646-1.656 2.336 3.918.789c-.157.413-.244.84-.33 1.267-.086.428-.173.855-.188 1.297l-2.777-.56-1.656 2.336 4.345.875c.461 6.913 5.547 13.127 12.565 14.54 3.883.783 7.825-.017 11.07-1.958l.748-3.705c-2.982 2.66-7.135 3.939-11.303 3.1-5.699-1.149-9.817-5.91-10.408-11.44l17.346 3.496 1.658-2.336-19.023-3.832c.015-.442.101-.868.187-1.295.072-.356.173-.856.33-1.27l20.162 4.063 1.657-2.336-20.768-4.184c2.636-4.806 8.255-7.677 14.025-6.515 4.489.904 7.5 3.624 9.227 7.197l1.65-2.3C41.48 4.325 37.82 1.512 33.33.608a15.04 15.04 0 0 0-2.64-.29zM48.142 27.07c-.916.016-1.871.15-2.866.405-2.578.658-4.6 1.9-6.068 3.726-1.818 2.29-2.34 4.959-1.563 8.006.184.718.438 1.437.762 2.158.325.721.938 1.838 1.84 3.356l-3.383.863.83 3.254 4.307-1.1c.485.86.84 1.736 1.068 2.63.532 2.08.492 3.968-.12 5.661-.614 1.694-1.523 3.107-2.731 4.242l2.486 3.415c.82-.881 1.78-1.656 2.88-2.327 1.101-.67 2.179-1.14 3.233-1.41a12.695 12.695 0 0 1 2.112-.351c.462-.025 1.501.002 3.12.08 1.62.077 2.637.102 3.052.074a10.53 10.53 0 0 0 1.914-.3 9.814 9.814 0 0 0 2.138-.829c.695-.365 1.644-1.004 2.846-1.92l-2.201-3.3c-1.52 1.261-2.82 2.031-3.904 2.308-.894.228-2.677.363-5.35.406-1.617.023-3.032.192-4.248.502-.952.243-1.968.671-3.043 1.29 1.014-1.836 1.643-3.54 1.889-5.116.245-1.576.206-3.003-.12-4.277a4.998 4.998 0 0 0-.912-1.827l6.24-1.593-.83-3.252-6.921 1.765c-1.505-2.409-2.464-4.426-2.88-6.052-.377-1.48-.162-2.88.647-4.202.81-1.322 2.026-2.19 3.653-2.605 1.523-.389 2.959-.218 4.308.514 1.35.732 2.36 1.948 3.03 3.65l3.78-1.598c-1.068-2.535-2.65-4.312-4.743-5.33-1.309-.636-2.726-.942-4.252-.916zm-34.989 1.004l-.74 3.627c-1.527-.311-8.78.156-10.004 6.154-1.088 5.336 3.572 7.804 6.91 8.98l-1.222 6.032c-2.369-1.2-2.45-3.15-2.485-4.756L.105 46.988c-.882 6.07 4.032 9.978 6.946 11.014l-.752 3.69 5.234 1.066.74-3.631c4.189.633 9.8-1.81 10.762-6.527 1.102-5.403-3.474-7.934-7.555-9.196l1.127-5.527c1.616.446 2.575 2.271 2.39 3.89l5.454 1.112c1.093-5.36-3.404-9.213-6.857-10.121l.74-3.63-5.18-1.054zm-1.767 8.793l-.934 4.58c-3.68-.808-2.96-4.711.934-4.58zm3.058 11.908c4.546 1.044 4.162 5.533-1.062 5.198l1.062-5.198z"/>
+</svg>
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/restrict-default.svg b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/restrict-default.svg
new file mode 100644
index 00000000..1573b23d
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/restrict-default.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
+ <path fill="#222" fill-rule="evenodd" d="M32.166 3.814A3.556 3.556 0 0 0 28.92 5.59L.477 54.855a3.556 3.556 0 0 0 3.08 5.334h56.886a3.556 3.556 0 0 0 3.08-5.334L35.08 5.59a3.556 3.556 0 0 0-2.914-1.776zm-.176 18.377c.938 0 1.712.313 2.324.938.625.612.938 1.393.938 2.344 0 .99-.326 2.838-.977 5.547l-1.035 4.257c-.364 1.51-.631 3.308-.8 5.391h-.86c-.091-1.484-.39-3.281-.898-5.39l-1.016-4.258c-.612-2.553-.918-4.382-.918-5.489 0-1.002.3-1.81.898-2.422.6-.612 1.38-.918 2.344-.918zm.04 21.27a3 3 0 0 1 2.226.937c.625.612.937 1.361.937 2.247 0 .872-.312 1.62-.937 2.246a3.032 3.032 0 0 1-2.227.918 3.097 3.097 0 0 1-2.246-.918 3.097 3.097 0 0 1-.918-2.246c0-.873.306-1.622.918-2.247.612-.625 1.36-.937 2.246-.937z"/>
+</svg>
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/restrict-design.svg b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/restrict-design.svg
new file mode 100644
index 00000000..f3166617
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/restrict-design.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
+ <path fill="#222" d="M15.375 0v15.375H0v1h15.375V31.5H0v1h15.375v15.125H0v1h15.375L0 64h64V0L52.87 11.133l-4.243 4.242h-.002V0h-1v15.375H32.5V0h-1v15.375H16.375V0h-1zm1 16.375H31.5V31.5H16.375V16.375zm16.125 0h15.125v.002L32.502 31.5H32.5V16.375zm18.457 15.111v.014h-.014l.014-.014zM16.375 32.5H31.5v.002L16.375 47.625V32.5zm33.568 0h1.014v15.125h-2.332V33.818l1.318-1.318zm-2.318 2.318v12.807H34.818l12.807-12.807zM33.818 48.625h13.807v2.332H32.5v-1.014l1.318-1.318zm14.807 0h2.332v2.332h-2.332v-2.332zM31.5 50.943v.014h-.014l.014-.014z"/>
+</svg>
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/restrict-fan-art.svg b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/restrict-fan-art.svg
new file mode 100644
index 00000000..fbb5a0d0
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/restrict-fan-art.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
+ <path fill="#222" fill-rule="evenodd" d="M21.145 2.008c-.77.02-1.43.58-1.586 1.373L8.879 41.908c-.352 1.777.872 3.473 2.656 3.785 1.784.313 3.511-.865 3.785-2.656l7.508-39.084c.14-.916-.443-1.76-1.35-1.92a1.665 1.665 0 0 0-.333-.025zm18.43 7.65c-1.161.024-2.37.165-3.634.446-5.145 1.14-10.539 3.35-14.744 7.72l-4.873 25.37c-.17 1.1-.782 2.002-1.6 2.634.45.816.658 1.855.55 3.23 3.81 9.697 13.185 12.934 18.292 12.934 6.76 0 22.29-3.106 21.377-11.328-.913-8.222-8.952-10.05-8.039-14.252.914-4.202 9.502 3.29 12.608 4.022 3.106.73 8.037-11.144.547-18.635-6.555-6.555-12.363-12.307-20.485-12.14zm.347 3.285c1.29-.021 2.636.63 3.6 1.739 1.612 1.854 1.694 4.422.183 5.736-1.512 1.314-4.046.875-5.658-.98s-1.695-4.425-.184-5.739c.552-.48 1.271-.742 2.059-.756zm-10.969 5.094c1.369.05 2.77.621 3.91 1.592 2.42 2.062 3.02 5.331 1.34 7.303-1.68 1.97-5.005 1.897-7.424-.164-2.418-2.062-3.017-5.33-1.338-7.301.822-.965 2.09-1.481 3.512-1.43zm-6.47 12.053a4.953 4.953 0 0 1 2.288.525c2.093 1.034 3.107 3.254 2.264 4.96-.843 1.705-3.223 2.25-5.316 1.216-2.093-1.035-3.106-3.257-2.262-4.963.52-1.051 1.66-1.707 3.025-1.738zm1.146 13.113c.087.003.173.01.26.018 2.03.188 3.552 1.683 3.398 3.338-.154 1.654-1.925 2.84-3.955 2.652-2.03-.189-3.55-1.682-3.396-3.336.145-1.572 1.759-2.74 3.693-2.672zM8.906 44.955c-1.095.544-2.222 1.507-3.177 2.906-1.998 2.928 1.04 5.007-.672 8.418C3.345 59.691 0 61.83 0 61.83s11.1-2.634 13.307-8.866c1.343-3.795 1.187-5.702.504-6.815a4.078 4.078 0 0 1-2.41.315 4.073 4.073 0 0 1-2.495-1.508z"/>
+</svg>
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/restrict-ihl.svg b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/restrict-ihl.svg
new file mode 100644
index 00000000..026c80a0
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/restrict-ihl.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
+ <path fill="#222" d="M34.923.061c-.539-.071-1.103.157-1.744.8-1.297 1.301-1.765 3.165-1.483 5.123L5.672 32.068c-1.864-.2-3.625.276-4.865 1.52-2.565 2.573 1.509 3.917 4.875 7.273l5.224 5.207c3.366 3.356 4.711 7.417 7.276 4.845 1.244-1.248 1.717-3.019 1.506-4.889l10.794-10.82c13.061 14.525 16.6 17.131 25.408 26.958.055.062.116.125.176.184 1.85 1.82 5.628 2.136 7.038.723 1.455-1.46 1.133-5.375-.926-7.211-9.848-8.783-12.547-12.23-27.106-25.255l10.86-10.887c1.773.132 3.436-.341 4.622-1.53 2.564-2.573-1.51-3.917-4.876-7.273l-5.223-5.207C37.93 3.19 36.542.275 34.923.061z"/>
+</svg>
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/restrict-insignia.svg b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/restrict-insignia.svg
new file mode 100644
index 00000000..6f8cecec
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/restrict-insignia.svg
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
+ <path fill="#222" d="M30.297 0a.736.736 0 0 0-.125.016c-.315-.028-1.217.426-.729.648.623-.087 1.127.204 1.266.82.427.425.432 1.249-.283 1.362-.615.126-1.18-.27-1.774-.368-.363-.59.887-.638.678-1.166-.283-.54-1.28-.793-1.441-.027-.236.572-.218 1.27-.703 1.717-.408.514-.335 1.22-.838 1.602-.055.71.859.67 1.187 1.128.347.4.814 1.002 1.412.702.697-.322.39-1.245.903-1.68.748-.556.288.791.511.955.372-.027.965-.115.774.467.26.452.103.558-.819 1.16-.498-.582-1.488-.1-1.336.144.508.164 2.103-.303 2.102.473-.209.715-1.088.808-1.69.57-.62-.11-1.236-.27-1.87-.265-.654-.045-1.28.33-1.66.844-.157.589-.738 1.308-.266 1.878.705.313 1.012-.675 1.572-.898.612-.337 1.361-.244 2.037-.285.674-.01 1.344-.015 2.008.103.262.016.524.026.787.034.263-.008.525-.018.787-.034.664-.118 1.334-.112 2.008-.103.676.041 1.425-.052 2.037.285.56.223.867 1.21 1.572.898.472-.57-.11-1.29-.265-1.878-.38-.515-1.007-.89-1.66-.844-.635-.004-1.25.156-1.872.265-.601.238-1.48.145-1.689-.57 0-.776 1.594-.31 2.102-.473.152-.243-.838-.726-1.336-.144-.923-.602-1.08-.708-.819-1.16-.19-.582.402-.493.774-.467.223-.164-.237-1.511.511-.955.513.435.206 1.358.903 1.68.598.3 1.065-.303 1.412-.702.328-.458 1.242-.418 1.187-1.128-.503-.382-.43-1.088-.838-1.602-.485-.448-.467-1.145-.703-1.717-.16-.766-1.158-.513-1.441.027-.21.528 1.041.576.678 1.166-.594.098-1.16.494-1.774.368-.715-.113-.71-.937-.283-1.362.139-.616.643-.907 1.266-.82.488-.222-.414-.676-.729-.648A.717.717 0 0 0 33.703 0c-.61-.017-.922.763-1.328 1.135-.084.098-.17.197-.232.31-.04.13-.088.255-.143.377a3.023 3.023 0 0 1-.143-.377c-.063-.113-.148-.212-.232-.31C31.219.763 30.907-.018 30.297 0zm2.213 11.518c-1.72.03-3.463.782-4.815 2.502-2.402 3.057-.436 2.768.729 4.296 1.165 1.53 1.238 4.732-.072 5.97 0 0 3.785-3.713 5.095-3.786 1.31-.073 3.057 1.53 3.057 1.53s-.8-2.113-.436-3.278c.364-1.165 2.767-4.44.22-6.115-1.116-.733-2.44-1.144-3.778-1.12zm-9.405.101c-.187-.013-.391.125-.63.512-.975-.062-1.694-.392-2.707-.025-1.617-.271-2.538 1.2-3.315 2.351-.258.99.71 1.623 1.582 1.536a.959.959 0 0 1 .625-.215c.013-.01.026-.014.04-.023 1.092-.543-.19-2.968 1.603-2.276.53.747.16 3.313-1.221 2.385a1.295 1.295 0 0 0-.422-.084c-.188.122-.403.19-.625.213-.585.452-.889 1.483-1.586 1.766-1.152.977.179 2.211.182 3.345.026 1.421 1.537 2.645 2.922 2.106 1.545-.068 2.055-1.638.61-2.366-.686-.95-1.826-1.375-2.606-2.216 1.697-.434 3.125.857 4.662 1.275 1.916.203 4.129-1.555 3.636-3.61.12-1.225-1.446-1.99-.992-3.314-.124-.082-.277.023-.379.084-.535-.384-.9-1.409-1.379-1.443zm17.79 0c-.479.035-.844 1.06-1.38 1.444-.101-.061-.252-.167-.376-.084.454 1.324-1.115 2.086-.994 3.312-.493 2.054 1.72 3.812 3.636 3.61 1.537-.418 2.965-1.71 4.662-1.276-.78.842-1.92 1.268-2.605 2.219-1.446.728-.936 2.297.61 2.365 1.384.54 2.895-.685 2.921-2.105.003-1.135 1.334-2.369.182-3.346-.697-.283-1.001-1.314-1.586-1.766a1.398 1.398 0 0 1-.625-.213c-.129 0-.267.028-.422.084-1.38.928-1.751-1.64-1.22-2.386 1.791-.692.51 1.733 1.603 2.275.013.01.026.017.039.025.246 0 .448.078.625.215.872.088 1.84-.546 1.582-1.535-.777-1.15-1.698-2.623-3.315-2.352-1.013-.367-1.732-.038-2.707.025-.239-.387-.443-.525-.63-.512zM9.742 19.611c-.056 0-.113 0-.17.012-.767.488-1.877.17-2.779.155-.527.56.018 1.03.103 1.543.477.908.072 2.124-.103 3.09-.507.93.744 1.186.412 2.162-.424.407-.6.975-.516 1.67.068.616-.162 1.112-.103 1.726.19.781.263 1.82.83 2.572.356.595.146 1.41.201 2.125.014 1.15.231 2.21-.27 3.328-.368 1.138-.931 2.282-1.171 3.3-.35.8-.865 1.92-1.237 2.882-.304.707-.355 2.178-1.441 1.545-.355-.637.207-1.514.104-2.266.22-.814.082-1.782.412-2.572.156-.946.088-1.983.101-2.988.078-.821-.115-1.186-.72-1.543-.286-.85-.582-1.65-.514-2.594-.181-.707-.078-1.518-.104-2.287-.07-.445.27-1.596-.513-1.09-.728.472-1.533 1.126-1.34 2.08-.21 1.42.961 2.484 1.236 3.79v4.96c.052.606-.113 1.297.104 1.834v2.37c-.233 1.1.555 1.649 1.142 2.411.031 1.832 1.417.066 1.43-.767-.086-.56.734-2.392.62-2.059.032 1.066-.441 2.047-.517 3.088v2.102c-.047.977.558 1.68.774 2.443.653.911.799 2.206 1.598 3.125-.118 1.579 2.086 1.245 2.261.236-.12-.735.232-1.279.207-1.976.085-.849-1.17-.88-1.644-1.42-.222-.794-.499-1.733-.21-2.553.386-.874.431-1.821 1.13-2.37.76-.379.778-1.342.724-2.161-.137-.837.18-1.02.823-.719-.058 1.058-.137 2.123-.102 3.192.191.713-1.859 1.376-1.256 1.855.93.165 1.8 1.307 2.902 1.129 1.207.071 2.59-.383 3.5-.926-.035-1.149.169-2.347-.412-3.398-.518-1.427-.734 1-1.853.31-.687-.386-1.105-.672-.926-1.547-.044-.992.102-2.03-.103-2.984-.153-1.02-1.793-.995-2.676-1.133-.998.443-.484-1.077-.53-.98.7-1.214 1.344-2.584 2.176-3.653.46.344 1.18 1.196 1.903 1.532.689.835 2.046 1.048 2.763.187.693-.538 1.118-1.904 1.432-2.111 1.016-.33.603-1.846-.332-1.975.837-1.114-.746-1.372-1.338-1.646-.448.07-2.044-.785-1.75-.824.6-.402-.108-.84.102-1.647.205-1.115 1.591-1.002 2.472-1.545 1.475-.868-1.064-2.426.617-3.09.102-.811-.57-.842-.72-1.338.086-.593-.113-1.066-.309-1.543-.88-1.325-1.257.172-1.558 1.012-.363 1.159-.854 2.522-1.737 3.31-.198.917-1.203 2.057-1.75.928.226-1.05.127-1.7-.926-1.914-.281-.412-.286-1.306-.103-1.69.55-.672 1.475-.561 1.955-1.235.705-.305.746-1.822 0-1.852-.656-.463-1.455-1.582-2.3-1.607zm44.516 0c-.846.026-1.645 1.143-2.301 1.606-.746.03-.705 1.549 0 1.853.48.675 1.406.564 1.955 1.237.183.382.178 1.277-.103 1.69-1.053.213-1.152.863-.926 1.913-.547 1.129-1.552-.012-1.75-.928-.883-.788-1.374-2.152-1.737-3.31-.3-.84-.678-2.34-1.558-1.014-.196.477-.395.952-.309 1.545-.15.495-.822.526-.72 1.338 1.68.664-.858 2.222.617 3.09.88.542 2.267.427 2.472 1.543.21.808-.497 1.246.102 1.648.294.04-1.302.895-1.75.824-.592.275-2.175.532-1.338 1.647-.935.128-1.348 1.644-.332 1.975.314.207.739 1.573 1.432 2.11.717.862 2.074.647 2.763-.189.722-.335 1.443-1.185 1.903-1.529.831 1.067 1.476 2.434 2.174 3.647-.013 0 .44 1.415-.528.986-.883.138-2.523.112-2.676 1.133-.205.954-.059 1.992-.103 2.984.179.874-.24 1.158-.926 1.545-1.119.69-1.335-1.735-1.853-.309-.58 1.052-.377 2.25-.412 3.399.91.543 2.293.997 3.5.926 1.103.178 1.973-.965 2.902-1.13.603-.479-1.447-1.141-1.256-1.855.035-1.068-.044-2.134-.102-3.191.643-.302.96-.118.823.719-.054.819-.037 1.782.724 2.16.699.55.744 1.496 1.13 2.37.289.82.012 1.76-.21 2.554-.473.54-1.729.57-1.644 1.42-.025.697.327 1.24.207 1.976.175 1.009 2.379 1.343 2.261-.236.8-.919.945-2.214 1.598-3.125.216-.763.82-1.466.774-2.444v-2.101c-.076-1.04-.55-2.022-.516-3.088-.115-.334.705 1.498.62 2.059.012.834 1.398 2.598 1.429.767.587-.763 1.375-1.312 1.142-2.412v-2.371c.217-.537.052-1.227.104-1.832v-4.963c.275-1.305 1.445-2.366 1.236-3.787.193-.955-.612-1.608-1.34-2.08-.784-.507-.443.644-.513 1.09-.026.768.077 1.58-.104 2.287.068.944-.228 1.743-.514 2.594-.605.357-.798.721-.72 1.543.013 1.005-.055 2.042.101 2.988.33.79.191 1.757.412 2.572-.103.752.46 1.629.104 2.266-1.086.632-1.137-.838-1.441-1.545-.372-.962-.887-2.082-1.237-2.883-.24-1.018-.803-2.161-1.172-3.299-.5-1.12-.283-2.18-.27-3.328.056-.714-.154-1.53.202-2.125.567-.753.64-1.791.83-2.572.059-.615-.17-1.112-.103-1.729.083-.694-.092-1.26-.516-1.668-.332-.975.919-1.232.412-2.162-.175-.965-.58-2.181-.103-3.09.085-.512.63-.983.103-1.543-.902.015-2.012.332-2.78-.156-.056-.01-.113-.012-.17-.01zm-34.79 5.39v17.991c0 6.958 5.61 12.6 12.532 12.6 6.921 0 12.532-5.642 12.531-12.6v-17.99zm.452 29.317a1.4 1.4 0 0 0-.574.133c-1.156.11-1.582 1.782-2.797 1.488-.987-.59-1.445 1.211-.502 1.397.424.535.836 1.127 1.422 1.486-6.91-.338-10.592-1.611-10.592-1.611.423 2.006.636 3.861 0 5.098 0 0 4.892 1.691 13.992 1.691 9.1 0 19.219-1.455 26.717-1.455 7.498 0 9.537 1.455 9.537 1.455-.396-1.401-.685-2.883 0-5.096 0 0-1.983-1.41-9.209-1.453.08-.12.157-.243.229-.369.708-.055.078-1.042-.147-1.379-1.236 1.111-1.955-.82-3.004-1.133-.973-.56-3.512.084-2.474 1.434.51.592.149 1.294-.606.787-1.157-.316-2.51.044-3.197.887-.17-.548-.707-1.28-.947-.307-.332.76-.998.585-.727-.205-.373-1.506-3.195-.419-1.676.746-.47.556-2.604.1-1.392-.863.525-.674-.072-1.72-.659-.656-.553.468-1.04 2.959-1.78 1.464-.308-.645-1.035-2.176-1.723-1.75.148.825 1.202 2.34-.442 1.987-.738.215-1.06-.308-.357-.682.224-1.51-2.644-1.163-2.092.418-.573.485-.69-1.624-1.51-.596-.327 1.097-.903-.834-1.808-.535-.686-.292-2.731.87-2.3-.465 1.2-.914-.255-1.946-1.382-1.916zm9.271 2.805l-.003.021zm5.616 0l.002.021z"/>
+ <path fill="none" d="M616.063-229.45s-19.799-14.141-92.63-14.141c-72.833 0-171.12 14.142-259.509 14.142-88.388 0-135.893-16.438-135.893-16.438 6.175-12.009 4.107-30.012 0-49.498 0 0 47.505 16.438 135.893 16.438s186.676-14.142 259.508-14.142 92.631 14.142 92.631 14.142c-6.654 21.49-3.849 35.885 0 49.498z"/>
+</svg>
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/restrict-ita-mibac.svg b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/restrict-ita-mibac.svg
new file mode 100644
index 00000000..492220ca
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/restrict-ita-mibac.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
+ <path fill="#222" d="M30.333 0c-2.038.777-5.011.9-6.296 1.932C21.898.845 22.13 3.129 21.941 3.44c-2.834-1.096.312 3.695-2.301 1.53-1.463 1.483-2.776-2.772-2.958.5-.336 1.576-1.55 2.906-2.411 1.237-1.258-.47-1.294-3.868-2.414-1.078-.23 3.548-6.552.574-5.698 4.106 2.332 1.41-.494 3.275-1.086 3.73 2.747 1.525-.643 5.362 3.002 5.757 2.684-.276.144 1.433 1.237 2.36 2.98-.145 4.12-5.818 7.549-3.489 2.196 1.381 5.189 2.538 5.098 5.507.896 1.315.17 3.456 1.843 3.937 1.111.681 1.67 2.425 1.941 2.707 3.367.791 4.577 4.81 7.552 6.402 2.06.69 5.108.291 5.566 3.31 1.845-.877.92 2.146 2.694.887 1.865.923 1.107 4.238 4.088 3.788 1.297 1.363 3.28 5.757 2.276 7.507-1.345.623-.872 2.94-2.03 3.698 1.408 2.932 4.08-1.39 4.459-3.12.56-2.335 4.26-1.664 1.986-4.607-1.224-1.336-3.36-1.734-1.794-3.904.453-2.184 2.617-2.798 3.993-1.455 2.022-.471 3.391 5.54 4.394 1.738-.419-3.44-4.418-4.727-6.968-6.335-1.781-.735-6.323-2.15-3.646-4.197-.839-2.147-4.687.465-6.298-1.895-2.546-1.748-4.23-4.364-4.698-7.426-1.01-3.574-6.77-3.743-6.716-7.992-1.055-1.63 2.211-1.957.34-3.497-.954-1.05.72-3.765.655-2.356 2.433-.589 3.195-2.72 5.615-1.48 1.4 1.723-.621-1.31-.45-1.264-1.293-1.154.955-1.941-.826-2.237-.032-1.179 2.253-2.251-.193-2.234-1.877.062-6.639-.972-5.353-3.36L30.333 0zM16.641 37.157c-.972 1.768-3.476 2.719-4.84 2.582.016 1.888 2.138 3.61 1.249 5.37.696 1.788-1.47 6.907 2.068 5.866.59-2.577 3.66-.06 3.51-2.975-.596-2.899 1.494-6.423-.452-8.816.512-1.086-1.026-1.395-1.535-2.027zm2.325 2.013c-1.444-.19-.416.642 0 0zm26.568 15.48c-2.47 1.015-5.208 1.696-7.86 1.785-1.738-.474-2.625-1.488-4.233-.351-2.767-1.675-2.33 3.99.588 3.14 2.893 1.72 6.007 3.487 8.96 4.776 2.757-1.698-1.014-5.466 1.987-7.524.253-.43 1.383-1.423.558-1.826z"/>
+</svg>
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/restrict-nazi.svg b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/restrict-nazi.svg
new file mode 100644
index 00000000..a784da88
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/restrict-nazi.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
+ <path fill="#222" fill-rule="evenodd" d="M32.008 0L12.805 19.195l6.398 6.399L38.406 6.398 32.008 0zm12.797 12.797l-6.399 6.398 19.196 19.203L64 32 44.805 12.797zM31.992 19.5c-.893 0-1.616.278-2.172.832-.555.554-.836 1.288-.836 2.195 0 1.002.284 2.655.852 4.965l.945 3.86c.471 1.91.744 3.529.828 4.873h.797c.157-1.886.404-3.506.742-4.873l.961-3.86c.604-2.451.907-4.122.907-5.017 0-.86-.288-1.568-.868-2.121-.567-.566-1.286-.854-2.156-.854zM6.398 25.602L0 32l19.203 19.203 6.39-6.398L6.399 25.602zm38.407 12.796L25.602 57.602 31.992 64l19.211-19.203-6.398-6.399zm-12.774.36c-.82 0-1.518.28-2.086.846-.567.565-.851 1.247-.851 2.037s.284 1.461.851 2.027c.58.554 1.277.832 2.086.832.81 0 1.495-.278 2.063-.832.58-.566.867-1.238.867-2.027a2.71 2.71 0 0 0-.867-2.037 2.81 2.81 0 0 0-2.063-.846z"/>
+</svg>
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/restrict-personality.svg b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/restrict-personality.svg
new file mode 100644
index 00000000..625ee415
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/restrict-personality.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
+ <path fill="#222" d="M0 0v64h64V0H0zm6 6h52v52h-6.06a25.206 60.413 0 0 0-10.19-18.945c-2.754 1.99-6.12 3.181-9.758 3.181-3.624 0-6.978-1.18-9.726-3.158A25.206 60.413 0 0 0 12.06 58H6V6zm25.992 6C24.541 12 18.5 18.04 18.5 25.492c0 7.452 6.04 13.492 13.492 13.492s13.492-6.04 13.492-13.492c0-7.451-6.04-13.492-13.492-13.492z"/>
+</svg>
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/time.svg b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/time.svg
new file mode 100644
index 00000000..3c635e77
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/time.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="80" height="80" viewBox="0 0 80 80">
+ <path fill="#72777d" d="M64.742 15.246C58.14 8.638 49.35 5 39.996 5c-9.348 0-18.137 3.638-24.754 10.246C8.633 21.856 5 30.65 5 40s3.633 18.144 10.242 24.742C21.859 71.352 30.648 75 39.996 75c9.354 0 18.144-3.649 24.746-10.258C71.353 58.144 75 49.35 75 40s-3.647-18.144-10.258-24.754zm-13.283 41.29L36.504 41.604V16.01h5.628v23.234l13.3 13.309-3.973 3.982z"/>
+</svg>
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/use-ltr.svg b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/use-ltr.svg
new file mode 100644
index 00000000..20f540ec
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/use-ltr.svg
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
+ <path fill-opacity=".51" d="M16.051 1.55a.52.52 0 0 0-.092.062.52.52 0 0 0-.245.153c.004-.004-.034.004-.03 0 .004-.004-.004.034 0 .03l-.03.031a.52.52 0 0 0 0 .03.52.52 0 0 0-.031 0c.002-.003-.003.035 0 .031l-.03.03c.001-.003-.033.035-.031.031.001-.003-.002.035 0 .031.001-.004-.033.035-.031.03v.061a.52.52 0 0 0-.03.123v.061a.582.582 0 0 0 0 .062v2.233c-4.157.277-6.5 2.397-7.589 4.59-1.141 2.299-1.103 4.542-1.101 4.59v.03a2.328 2.328 0 0 0 0 .092.52.52 0 0 0 .06.184.52.52 0 0 0 0 .03c0-.004.033.035.031.03-.001-.003.002.035 0 .031a.52.52 0 0 0 .03.031c-.002-.004.035.034.032.03-.003-.003.002.035 0 .031a.52.52 0 0 0 .03.03c-.002-.001.024-.007.03 0 .008.009.002.033 0 .031-.003-.004.004.035 0 .031-.003-.004.035.004.031 0l.03.03a.52.52 0 0 0 .032.031c-.004-.002.034.002.03 0l.03.03c-.003-.001.035.033.031.031-.003-.001.035.002.03 0-.003-.001.035.033.031.031-.003-.002.035.001.031 0h.03a.52.52 0 0 0 .123.03h.061a.452.452 0 0 0 .061 0h.061a.52.52 0 0 0 .123-.03h.03c-.003.001.035-.002.031 0-.004.002.035-.032.03-.03-.003.001.035-.002.031 0-.003.001.035-.033.031-.031-.004.002.034-.033.03-.03-.003.001.035-.003.031 0a.52.52 0 0 0 .03-.031l.031-.031c-.003.004.035-.004.031 0 .008-.007.002-.032 0-.03l.03-.031.031-.03a.52.52 0 0 0 .03-.031S10.79 10.6 15.5 10.975v2.845a.52.52 0 0 0 .03.123.15.15 0 0 0 0 .03.16.16 0 0 0 0 .031c0-.004.033.034.032.03-.002-.003.001.035 0 .031-.002-.004.032.034.03.03-.002-.003.002.035 0 .031.005.008.032.002.03 0-.001-.003.003.034 0 .031l.031.03c-.002-.003.003.034 0 .031-.002-.003.033.003.03 0l.031.03c-.002-.002.003.034 0 .031-.002-.002.034.003.031 0l.03.031c-.002-.003.004.033 0 .03-.002-.002.035.003.031 0-.003-.002.034.033.03.031-.002-.002.035.002.031 0-.003-.002.004.033 0 .03.01.007.034.002.031 0l.061.031c-.005-.002.036.003.03 0-.005-.002.037.033.031.031-.005-.002.037.002.031 0l.061.03h.061a.52.52 0 0 0 .031 0h.122a.52.52 0 0 0 .184-.06c-.004 0 .035-.002.03 0-.003.001.035-.033.031-.031a.52.52 0 0 0 .061-.03l.031-.031c-.004.003.034-.004.03 0-.003.003.004-.035 0-.031l.031-.03a.52.52 0 0 0 .03 0l5.814-5.722a.52.52 0 0 0 .062-.062c-.003.004.002-.034 0-.03-.003.003.032-.034.03-.03-.002.003.002-.035 0-.031l.03-.031c0 .004.033-.034.031-.03-.001.003.002-.035 0-.031a.071.071 0 0 1 0-.03c.004-.009.032-.003.031 0-.002.003.001-.035 0-.031V8.22a.52.52 0 0 0 .03-.122v-.122a.52.52 0 0 0-.03-.123v-.03c.001.003-.002-.035 0-.031 0 .002-.027.007-.03 0 0 .004-.002-.035 0-.03 0 .003-.002-.035 0-.032.001.004-.033-.034-.031-.03 0 .002.004-.024 0-.03 0 .001-.027.007-.03 0 .001.003-.003-.035 0-.031.001.003-.034-.034-.031-.03.002.003-.003-.035 0-.032a.52.52 0 0 0-.062-.06l-5.813-5.753a.52.52 0 0 0-.061-.061c.003.002-.034-.003-.031 0l-.03-.03a.06.06 0 0 1-.031 0c.003.001-.034-.034-.03-.032a.439.439 0 0 1-.031-.03c.003.002-.035-.002-.031 0a.071.071 0 0 1-.03 0c-.008-.003-.003-.031 0-.03.003 0-.035-.002-.031 0 .004 0-.035-.002-.03 0a.52.52 0 0 0-.123-.031h-.183a.52.52 0 0 0-.062 0zM.63 3.08a.52.52 0 0 0-.153.092.52.52 0 0 0-.184.123c.003-.003-.003.033 0 .03a.52.52 0 0 0-.03.031c.002-.003-.033.003-.031 0a.52.52 0 0 0 0 .03l-.03.031c.002-.003-.003.034 0 .03.001-.003-.033.004-.031 0-.006.011-.002.034 0 .031a4.58 4.58 0 0 1-.03.062c.002-.006-.003.036 0 .03.001-.005-.033.036-.031.03.001-.005-.002.037 0 .031.001-.005-.032.067-.031.062v.06a.52.52 0 0 0 0 .062v16.798a.52.52 0 0 0 .03.123.15.15 0 0 0 0 .03.15.15 0 0 0 0 .03c0-.003.033.035.031.031-.002-.003.002.035 0 .03l.03.032c-.001-.004.003.034 0 .03.005.007.033.002.031 0-.002-.003.003.034 0 .03-.002-.003.033.035.031.031-.002-.003.002.034 0 .03-.002-.002.033.004.03 0l.031.032c-.003-.003.003.033 0 .03-.003-.003.034.003.03 0l.031.03c-.003-.002.004.034 0 .031-.003-.002.034.003.031 0l.03.03c-.003-.001.035.003.031 0-.003-.001.004.033 0 .032.01.005.033 0 .03 0l.062.03c-.005-.002.036.002.03 0-.005-.002.037.033.031.03-.005-.001.036.002.03 0-.005 0 .068.033.062.031h.061a.52.52 0 0 0 .061 0h18.175a.52.52 0 0 0 .123-.03c-.004.001.034-.002.03 0-.003.001.035-.002.031 0-.004.001.034-.033.03-.03-.003 0 .035-.003.031 0-.003.001.034-.034.03-.031-.003.002.035-.033.031-.031-.003.002.034-.003.031 0l.03-.03c-.003.002.034-.003.031 0-.003.002.003-.034 0-.031l.03-.03c-.002.002.034-.004.031 0-.002.002.003-.034 0-.031l.031-.031c-.003.003.033-.003.03 0-.002.003.003-.034 0-.03-.002.003.033-.035.031-.031-.002.003.002-.034 0-.03-.003.004.034-.037.03-.031l.031-.062c-.002.006.003-.036 0-.03-.002.005.033-.036.031-.03-.002.005.002-.037 0-.031l.03-.062v-.06a.52.52 0 0 0 0-.062v-7.283a.52.52 0 0 0-.03-.123v-.03c.001.004-.002-.035 0-.03l-.03-.032c.001.004-.002-.034 0-.03.001.004-.033-.034-.031-.03 0 .001.004-.024 0-.031.002.003-.033-.004-.03 0 .001.003-.003-.034 0-.03l-.031-.032c.002.004-.003-.033 0-.03.002.003-.034-.003-.031 0l-.03-.03c.002.002-.003-.034 0-.031.002.003-.034-.003-.031 0l-.03-.03c.002.002-.004-.034 0-.031.002.002-.035-.003-.031 0l-.031-.031c.003.002-.034-.002-.03 0 .003.002-.004-.033 0-.03-.011-.007-.034-.002-.031 0a4.144 4.144 0 0 1-.061-.031c.005.002-.036-.002-.031 0 .006.002-.036-.033-.03-.03.005.001-.037-.002-.031 0 .006 0-.067-.033-.061-.031h-.062a.52.52 0 0 0-.03 0h-.123a.52.52 0 0 0-.153.06.52.52 0 0 0-.091.031c.004-.001-.035.002-.03 0l-.032.03c.004-.001-.034.034-.03.032.003-.003-.034.002-.03 0a.52.52 0 0 0-.031.06.52.52 0 0 0-.03 0l-2.663 2.602a.52.52 0 0 0-.061.06c.002-.003-.002.035 0 .031l-.03.03c.001-.003-.033.035-.031.032.002-.004-.002.034 0 .03.001-.004-.032.035-.03.03 0-.003-.002.035 0 .031 0-.002.002.023 0 .03-.004.009-.032.003-.031 0 .001-.003-.002.035 0 .031 0-.003-.002.035 0 .031a.52.52 0 0 0-.031.122v1.286H4.24V7.15h4.069a.52.52 0 0 0 .092-.03.52.52 0 0 0 .153-.031c-.004.002.034-.033.03-.03-.003.001.034-.034.03-.031-.003.002.035-.003.031 0l3.948-2.601a.52.52 0 0 0 .06-.03l.031-.031.03-.03c-.004.004.036-.067.032-.062-.005.005.034-.036.03-.03-.004.005.034-.037.03-.031 0 .002-.004-.022 0-.03-.002.003.033-.035.031-.031-.003.006.033-.037.03-.031 0 .003-.004-.018 0-.03a.071.071 0 0 1 0-.031c.004-.008.032-.002.031 0-.001.004.002-.035 0-.03 0 .003.002-.035 0-.031a.52.52 0 0 0 .031-.123v-.122a.52.52 0 0 0-.03-.123c0 .004-.002-.034 0-.03 0 .004-.002-.035 0-.03 0 .001-.028.007-.031 0V3.54c.001.003-.002-.035 0-.031.001.004-.032-.034-.03-.03.001.003-.003-.035 0-.031 0 .002-.027.007-.031 0 .002.003-.003-.034 0-.03l-.03-.031c.001.003-.003-.034 0-.031a.52.52 0 0 0-.062-.061 3.266 3.266 0 0 1-.03-.03l-.031-.031c.003.002-.034-.003-.03 0 .003.002-.035-.033-.031-.031l-.031-.03c.004.001-.034-.033-.03-.031.003.002-.035-.002-.031 0 .004.002-.035-.002-.03 0 .003.001-.035-.032-.031-.03.004 0-.035-.002-.031 0 .004 0-.035-.002-.03 0 .003 0-.035-.032-.031-.031h-.061a.52.52 0 0 0-.031 0H.69a.52.52 0 0 0-.061 0z"/>
+ <path fill="#fff" d="M16.064 2.188v2.873c-8.872.057-8.7 8.606-8.7 8.606s3.163-4.297 8.7-3.442v3.442l5.797-5.734-5.797-5.745zM.624 3.723V20.46H18.75v-7.227l-2.655 2.602v1.97H3.331V6.336H8.08l3.94-2.613H.625z"/>
+</svg>
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/use-rtl.svg b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/use-rtl.svg
new file mode 100644
index 00000000..b16507cf
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/use-rtl.svg
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
+ <path fill-opacity=".51" d="M6.81 1.55a.52.52 0 0 0 .092.062.52.52 0 0 0 .244.153c-.003-.003.035.004.031 0-.004-.004.004.035 0 .03l.03.031a.52.52 0 0 0 0 .03.52.52 0 0 0 .031 0c-.002-.003.003.035 0 .031l.03.031c-.001-.004.033.034.031.03-.001-.003.002.035 0 .031-.001-.004.033.035.031.03-.002-.003.001.035 0 .031v.031a.52.52 0 0 0 .03.122V4.55c4.157.278 6.5 2.398 7.589 4.59 1.141 2.299 1.103 4.542 1.101 4.59v.122a.52.52 0 0 0-.06.184.52.52 0 0 0 0 .03c0-.003-.033.035-.031.031.001-.004-.002.034 0 .03a.52.52 0 0 0-.03.031c.002-.004-.035.035-.032.03.003-.003-.002.035 0 .031a.52.52 0 0 0-.03.031c.002-.002-.024-.008-.03 0-.008.008-.002.033 0 .03.003-.003-.004.035 0 .031.003-.003-.035.004-.031 0l-.03.03a.52.52 0 0 0-.032.031c.004-.002-.034.003-.03 0 .003-.002-.034.033-.03.031.003-.002-.035.032-.031.03.003-.001-.035.002-.03 0 .003-.001-.035.033-.032.031.004-.001-.034.002-.03 0h-.03a.52.52 0 0 0-.123.03h-.091a.2.2 0 0 0-.031 0h-.061a.52.52 0 0 0-.123-.03h-.03c.003.002-.035-.001-.031 0 .004.002-.035-.032-.03-.03.003.001-.035-.002-.031 0 .003.001-.035-.033-.031-.031l-.03-.03c.003.002-.035-.003-.031 0a.52.52 0 0 0-.03-.031l-.031-.03c.003.003-.035-.004-.031 0-.008-.008-.002-.033 0-.031l-.03-.031-.031-.03a.52.52 0 0 0-.03-.031S12.071 10.6 7.36 10.975v2.846a.52.52 0 0 0-.031.122v.03c.001-.003-.002.035 0 .031.002-.004-.032.035-.03.03.001-.003-.002.035 0 .031.001-.003-.033.035-.031.031.002-.003-.002.034 0 .03-.005.008-.032.002-.03 0 .001-.003-.003.035 0 .031l-.031.03c.002-.002-.003.035 0 .031.002-.003-.034.003-.03 0l-.032.031c.003-.003-.002.033 0 .03.003-.002-.033.003-.03 0l-.03.031c.002-.002-.004.033 0 .03.002-.002-.035.003-.031 0l-.03.031c.002-.002-.035.002-.032 0 .004-.002-.003.033 0 .031-.01.006-.033.001-.03 0a4.144 4.144 0 0 0-.061.03c.005-.002-.036.003-.03 0 .005-.002-.037.033-.032.031.006-.002-.036.002-.03 0 .006-.001-.067.032-.061.03.005 0-.037.002-.03 0h-.032a.52.52 0 0 0-.03 0h-.123a.52.52 0 0 0-.183-.06c.004.001-.035-.002-.03 0 .003.001-.035-.033-.031-.031a.52.52 0 0 0-.061-.03l-.031-.031c.004.003-.034-.004-.03 0 .003.004-.004-.035 0-.03l-.031-.031a.52.52 0 0 0-.03 0L.292 8.558a.52.52 0 0 0-.062-.061c.003.003-.002-.034 0-.031.003.003-.032-.034-.03-.03.002.003-.003-.035 0-.031l-.03-.03c0 .003-.033-.035-.031-.031.001.003-.002-.035 0-.031a.071.071 0 0 0 0-.03c-.004-.008-.032-.002-.031 0v-.061a.52.52 0 0 0-.03-.123v-.122a.52.52 0 0 0 .03-.123v-.061c0 .002.027.008.03 0 0 .004.002-.035 0-.03 0 .003.002-.035 0-.031-.001.004.033-.035.031-.03a.067.067 0 0 1 0-.032c0 .002.027.008.03 0-.002.004.003-.033 0-.03-.001.003.034-.034.031-.03-.002.003.003-.035 0-.031a.52.52 0 0 0 .062-.062l5.813-5.752a.52.52 0 0 0 .061-.061c-.003.002.034-.002.03 0l.032-.03a.06.06 0 0 0 .03 0c-.003.002.034-.033.03-.031l.031-.03c-.003 0 .035-.003.03 0a.071.071 0 0 0 .032 0c.007-.004.001-.032 0-.032-.004.002.034 0 .03 0-.004.002.035 0 .03 0a.52.52 0 0 0 .123-.03h.03a2.328 2.328 0 0 0 .123 0h.03a.52.52 0 0 0 .062 0zm15.42 1.53a.52.52 0 0 0 .153.093.52.52 0 0 0 .184.122c-.003-.003.003.034 0 .03a.52.52 0 0 0 .03.031c-.002-.003.033.003.031 0a.52.52 0 0 0 0 .03l.03.031c-.002-.003.003.034 0 .031-.001-.003.033.003.031 0 .006.01.002.033 0 .03l.03.062c-.002-.006.003.036 0 .03-.001-.005.033.037.031.03-.001-.005.002.037 0 .032-.001-.006.032.067.031.06v.061a.52.52 0 0 0 0 .062v16.798a.52.52 0 0 0-.03.123c0-.004-.002.034 0 .03 0-.004-.002.035 0 .031.001-.004-.033.034-.031.03.002-.003-.002.035 0 .031.002-.004-.033.034-.03.03.001-.003-.003.035 0 .031-.005.008-.033.002-.031 0 .002-.003-.003.034 0 .031.002-.003-.033.034-.031.03.002-.003-.002.034 0 .031.002-.003-.033.003-.03 0l-.031.03c.003-.002-.003.034 0 .031.003-.002-.034.003-.03 0l-.031.031c.003-.003-.004.033 0 .03.003-.002-.034.003-.031 0 .003-.002-.034.033-.03.031.003-.002-.035.002-.031 0 .003-.002-.004.033 0 .03-.01.007-.033.002-.03 0a4.263 4.263 0 0 0-.062.031c.005-.002-.036.003-.03 0 .005-.002-.037.033-.031.031.005-.002-.036.002-.03 0l-.062.03h-.061a.52.52 0 0 0-.061 0H3.872a.52.52 0 0 0-.122-.03h-.03c.003.001-.035-.002-.031 0 .004.002-.035-.032-.03-.03.003.001-.035-.002-.031 0 .003.001-.035-.033-.03-.031l-.032-.03c.004.001-.033-.003-.03 0 .003.001-.034-.034-.03-.031.003.002-.034-.003-.031 0 .003.002-.003-.034 0-.031l-.03-.03c.002.002-.034-.003-.031 0 .002.002-.003-.034 0-.031l-.031-.03c.003.002-.033-.004-.03 0 .002.002-.003-.035 0-.031l-.031-.031c.002.003-.002-.034 0-.03l-.03-.031a4.143 4.143 0 0 0-.031-.061c.002.005-.003-.036 0-.031.002.005-.033-.036-.031-.03.002.005-.002-.037 0-.031.001.005-.032-.067-.03-.061 0 .005-.002-.037 0-.031v-.03a.52.52 0 0 0 0-.062V13.27a.52.52 0 0 0 .03-.122v-.061c-.002.003.032-.035.03-.031-.001.004.002-.035 0-.03-.001.003.033-.035.031-.031-.001.001-.004-.024 0-.031-.002.003.033-.004.03 0-.001.003.003-.034 0-.03l.031-.031c-.002.003.003-.034 0-.03-.002.002.034-.004.03 0l.032-.031c-.003.002.002-.034 0-.031-.003.003.033-.003.03 0l.03-.03c-.002.002.004-.034 0-.031-.002.002.035-.003.031 0l.03-.03c-.002.001.035-.003.031 0-.003.001.004-.033 0-.031.01-.006.034-.002.031 0l.061-.031c-.005.002.036-.002.03 0-.005.002.037-.033.031-.03-.005.001.037-.002.031 0-.006 0 .067-.032.061-.031h.061a.52.52 0 0 0 .031 0h.122a.52.52 0 0 0 .153.06.52.52 0 0 0 .092.032c-.004-.002.035.001.03 0-.003-.002.035.032.031.03l.031.03c-.003-.002.034.003.03 0a.52.52 0 0 0 .031.062.52.52 0 0 0 .03 0l2.663 2.6a.52.52 0 0 0 .061.062c-.002-.003.002.034 0 .03l.03.031c-.001-.003.033.034.031.03-.002-.003.002.035 0 .031-.001-.003.032.035.03.03 0-.003.002.035 0 .032a.071.071 0 0 0 0 .03c.004.008.032.002.031 0-.001-.004.002.035 0 .03a.16.16 0 0 0 0 .031.52.52 0 0 0 .031.123v.03a.452.452 0 0 0 0 .062v1.193h11.29V7.15h-4.068a.52.52 0 0 0-.093-.03.52.52 0 0 0-.153-.03c.004.001-.034-.033-.03-.031.003.002-.034-.033-.03-.031.003.002-.035-.003-.031 0l-3.948-2.6a.52.52 0 0 0-.06-.032l-.031-.03a1.374 1.374 0 0 0-.03-.03c.004.004-.036-.067-.032-.062.005.005-.034-.036-.03-.03.004.005-.034-.037-.03-.031 0 .002.004-.021 0-.03.002.003-.033-.035-.031-.031.003.006-.034-.037-.03-.03 0 .002.004-.019 0-.031a.071.071 0 0 0 0-.031c-.004-.008-.032-.002-.032 0 .002.004 0-.035 0-.03.002.003 0-.035 0-.031a.52.52 0 0 0-.03-.123v-.122a.52.52 0 0 0 .03-.122c0 .004.002-.035 0-.031 0 .004.002-.035 0-.03 0 .001.028.007.031 0V3.54c-.001.004.002-.035 0-.03-.001.003.032-.035.03-.031-.001.003.003-.035 0-.031 0 .002.027.007.031 0-.002.003.003-.034 0-.03l.03-.031c-.001.003.003-.034 0-.03a.52.52 0 0 0 .062-.062l.03-.03.031-.031c-.003.002.034-.003.03 0l.031-.03.031-.031c-.004.002.034-.033.03-.031-.003.002.035-.002.031 0-.004.002.035-.002.03 0-.003.002.035-.032.031-.03-.004 0 .035-.002.031 0-.004 0 .035-.002.03 0-.003 0 .035-.032.031-.031h.061a.52.52 0 0 0 .031 0h11.505a.452.452 0 0 1 .062 0 .52.52 0 0 0 .061 0z"/>
+ <path fill="#fff" d="M6.798 2.189V5.06c8.873.057 8.7 8.606 8.7 8.606s-3.162-4.296-8.7-3.442v3.442L1.002 7.933 6.798 2.19zm15.44 1.534V20.46H4.113v-7.227l2.654 2.603v1.97h12.765V6.336h-4.75l-3.94-2.613h11.396z"/>
+</svg>
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/user-ltr.svg b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/user-ltr.svg
new file mode 100644
index 00000000..a631d5a0
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/user-ltr.svg
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="80" height="80" viewBox="0 0 80 80">
+ <path fill="#72777d" d="M16.436 8C8.396 8 8.014 15.667 8 16.049V72h55.562c8.039 0 8.422-7.667 8.438-8.049V8H16.436z"/>
+ <path fill="#fff" d="M31.202 26.303a4.655 4.655 0 1 1-.012 9.31 4.655 4.655 0 0 1 .012-9.31z"/>
+ <circle cx="50.476" cy="30.957" r="4.654" fill="#fff"/>
+ <path fill="#fff" d="M40.835 57.729c-7.189 0-13.987-2.433-18.186-6.508a2.666 2.666 0 1 1 3.715-3.827c3.226 3.131 8.636 5 14.471 5 5.749 0 11.117-1.83 14.359-4.896a2.667 2.667 0 0 1 3.665 3.875c-4.271 4.04-10.84 6.356-18.024 6.356z"/>
+</svg>
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/user-rtl.svg b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/user-rtl.svg
new file mode 100644
index 00000000..dcfeecfa
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/user-rtl.svg
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="80" height="80" viewBox="0 0 80 80">
+ <path fill="#72777d" d="M63.564 8c8.04 0 8.422 7.667 8.436 8.049V72H16.438C8.399 72 8.016 64.333 8 63.951V8h55.564z"/>
+ <path fill="#fff" d="M48.798 26.303a4.655 4.655 0 1 0 .012 9.31 4.655 4.655 0 0 0-.012-9.31z"/>
+ <circle cx="50.476" cy="30.957" r="4.654" fill="#fff" transform="matrix(-1 0 0 1 80 0)"/>
+ <path fill="#fff" d="M39.165 57.729c7.189 0 13.987-2.433 18.186-6.508a2.666 2.666 0 1 0-3.715-3.827c-3.226 3.131-8.636 5-14.471 5-5.749 0-11.117-1.83-14.359-4.896a2.667 2.667 0 0 0-3.665 3.875c4.271 4.04 10.84 6.356 18.024 6.356z"/>
+</svg>
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/x_gray.svg b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/x_gray.svg
new file mode 100644
index 00000000..2f0e4440
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/x_gray.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96">
+ <path fill="#a2a9b1" d="M96 14L82 0 48 34 14 0 0 14l34 34L0 82l14 14 34-34 34 34 14-14-34-34z"/>
+</svg>
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/x_white.svg b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/x_white.svg
new file mode 100644
index 00000000..75521e7e
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/img/x_white.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96">
+ <polygon fill="#fff" points="96,14 82,0 48,34 14,0 0,14 34,48 0,82 14,96 48,62 82,96 96,82 62,48 "/>
+</svg>
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.canvas.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.canvas.js
new file mode 100644
index 00000000..61d33624
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.canvas.js
@@ -0,0 +1,467 @@
+/*
+ * This file is part of the MediaWiki extension MediaViewer.
+ *
+ * MediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $, oo ) {
+ var C;
+
+ /**
+ * UI component that contains the multimedia element to be displayed.
+ * This first version assumes an image but it can be extended to other
+ * media types (video, sound, presentation, etc.).
+ *
+ * @class mw.mmv.ui.Canvas
+ * @extends mw.mmv.ui.Element
+ * @constructor
+ * @param {jQuery} $container Canvas' container
+ * @param {jQuery} $imageWrapper
+ * @param {jQuery} $mainWrapper
+ */
+ function Canvas( $container, $imageWrapper, $mainWrapper ) {
+ mw.mmv.ui.Element.call( this, $container );
+
+ /**
+ * @property {boolean}
+ * @private
+ */
+ this.dialogOpen = false;
+
+ /**
+ * @property {mw.mmv.ThumbnailWidthCalculator}
+ * @private
+ */
+ this.thumbnailWidthCalculator = new mw.mmv.ThumbnailWidthCalculator();
+
+ /**
+ * Contains image.
+ * @property {jQuery}
+ */
+ this.$imageDiv = $( '<div>' )
+ .addClass( 'mw-mmv-image' );
+
+ this.$imageDiv.appendTo( this.$container );
+
+ /**
+ * Container of canvas and controls, needed for canvas size calculations.
+ * @property {jQuery}
+ * @private
+ */
+ this.$imageWrapper = $imageWrapper;
+
+ /**
+ * Main container of image and metadata, needed to propagate events.
+ * @property {jQuery}
+ * @private
+ */
+ this.$mainWrapper = $mainWrapper;
+
+ /**
+ * Raw metadata of current image, needed for canvas size calculations.
+ * @property {mw.mmv.LightboxImage}
+ * @private
+ */
+ this.imageRawMetadata = null;
+ }
+ oo.inheritClass( Canvas, mw.mmv.ui.Element );
+ C = Canvas.prototype;
+
+ /**
+ * Maximum blownup factor tolerated
+ * @property MAX_BLOWUP_FACTOR
+ * @static
+ */
+ Canvas.MAX_BLOWUP_FACTOR = 11;
+
+ /**
+ * Blowup factor threshold at which blurring kicks in
+ * @property BLUR_BLOWUP_FACTOR_THRESHOLD
+ * @static
+ */
+ Canvas.BLUR_BLOWUP_FACTOR_THRESHOLD = 2;
+
+ /**
+ * Clears everything.
+ */
+ C.empty = function () {
+ this.$imageDiv.addClass( 'empty' ).removeClass( 'error' );
+
+ this.$imageDiv.empty();
+ };
+
+ /**
+ * Sets image on the canvas; does not resize it to fit. This is used to make the placeholder
+ * image available; it will be resized and displayed by #maybeDisplayPlaceholder().
+ * FIXME maybeDisplayPlaceholder() receives the placeholder so it is very unclear why this
+ * is necessary at all (apart from setting the LightboxImage, which is used in size calculations).
+ *
+ * @param {mw.mmv.LightboxImage} imageRawMetadata
+ * @param {jQuery} $imageElement
+ */
+ C.set = function ( imageRawMetadata, $imageElement ) {
+ this.$imageDiv.removeClass( 'empty' );
+
+ this.imageRawMetadata = imageRawMetadata;
+ this.$image = $imageElement;
+ this.setUpImageClick();
+
+ this.$imageDiv.html( this.$image );
+ };
+
+ /**
+ * Resizes image to the given dimensions and displays it on the canvas.
+ * This is used to display the actual image; it assumes set function was already called before.
+ *
+ * @param {mw.mmv.model.Thumbnail} thumbnail thumbnail information
+ * @param {HTMLImageElement} imageElement
+ * @param {mw.mmv.model.ThumbnailWidth} imageWidths
+ */
+ C.setImageAndMaxDimensions = function ( thumbnail, imageElement, imageWidths ) {
+ var $image = $( imageElement );
+
+ // we downscale larger images but do not scale up smaller ones, that would look ugly
+ if ( thumbnail.width > imageWidths.cssWidth ) {
+ imageElement.width = imageWidths.cssWidth;
+ imageElement.height = imageWidths.cssHeight;
+ }
+
+ if ( !this.$image.is( imageElement ) ) { // http://bugs.jquery.com/ticket/4087
+ this.$image.replaceWith( $image );
+ this.$image = $image;
+
+ // Since the image element got replaced, we need to rescue the dialog-open class.
+ this.$image.toggleClass( 'mw-mmv-dialog-is-open', this.dialogOpen );
+
+ this.setUpImageClick();
+ }
+ };
+
+ /**
+ * Handles a "dialog open/close" event from dialogs on the page.
+ *
+ * @param {jQuery.Event} e
+ */
+ C.handleDialogEvent = function ( e ) {
+ switch ( e.type ) {
+ case 'mmv-download-opened':
+ this.downloadOpen = true;
+ break;
+ case 'mmv-download-closed':
+ this.downloadOpen = false;
+ break;
+ case 'mmv-reuse-opened':
+ this.reuseOpen = true;
+ break;
+ case 'mmv-reuse-closed':
+ this.reuseOpen = false;
+ break;
+ case 'mmv-options-opened':
+ this.optionsOpen = true;
+ break;
+ case 'mmv-options-closed':
+ this.optionsOpen = false;
+ break;
+ }
+
+ this.dialogOpen = this.reuseOpen || this.downloadOpen || this.optionsOpen;
+ this.$image.toggleClass( 'mw-mmv-dialog-is-open', this.dialogOpen );
+ };
+
+ /**
+ * Registers click listener on the image.
+ */
+ C.setUpImageClick = function () {
+ var canvas = this;
+
+ this.handleEvent( 'mmv-reuse-opened', $.proxy( this.handleDialogEvent, this ) );
+ this.handleEvent( 'mmv-reuse-closed', $.proxy( this.handleDialogEvent, this ) );
+ this.handleEvent( 'mmv-download-opened', $.proxy( this.handleDialogEvent, this ) );
+ this.handleEvent( 'mmv-download-closed', $.proxy( this.handleDialogEvent, this ) );
+ this.handleEvent( 'mmv-options-opened', $.proxy( this.handleDialogEvent, this ) );
+ this.handleEvent( 'mmv-options-closed', $.proxy( this.handleDialogEvent, this ) );
+
+ this.$image.on( 'click.mmv-canvas', function ( e ) {
+ // ignore clicks if the metadata panel or one of the dialogs is open - assume the intent is to
+ // close it in this case; that will be handled elsewhere
+ if (
+ !canvas.dialogOpen &&
+ // FIXME a UI component should not know about its parents
+ canvas.$container.closest( '.metadata-panel-is-open' ).length === 0
+ ) {
+ e.stopPropagation(); // don't let $imageWrapper handle this
+ mw.mmv.actionLogger.log( 'view-original-file' ).always( function () {
+ $( document ).trigger( 'mmv-viewfile' );
+ } );
+ }
+ } );
+
+ // open the download panel on right clicking the image
+ this.$image.on( 'mousedown.mmv-canvas', function ( e ) {
+ if ( e.which === 3 ) {
+ mw.mmv.actionLogger.log( 'right-click-image' );
+ if ( !canvas.downloadOpen ) {
+ $( document ).trigger( 'mmv-download-open', e );
+ e.stopPropagation();
+ }
+ }
+ } );
+ };
+
+ /**
+ * Registers listeners.
+ */
+ C.attach = function () {
+ var canvas = this;
+
+ $( window ).on( 'resize.mmv-canvas', $.debounce( 100, function () {
+ canvas.$mainWrapper.trigger( $.Event( 'mmv-resize-end' ) );
+ } ) );
+
+ this.$imageWrapper.on( 'click.mmv-canvas', function () {
+ if ( canvas.$container.closest( '.metadata-panel-is-open' ).length > 0 ) {
+ canvas.$mainWrapper.trigger( 'mmv-panel-close-area-click' );
+ }
+ } );
+ };
+
+ /**
+ * Clears listeners.
+ */
+ C.unattach = function () {
+ this.clearEvents();
+
+ $( window ).off( 'resize.mmv-canvas' );
+
+ this.$imageWrapper.off( 'click.mmv-canvas' );
+ };
+
+ /**
+ * Sets page thumbnail for display if blowupFactor <= MAX_BLOWUP_FACTOR. Otherwise thumb is not set.
+ * The image gets also blured to avoid pixelation if blowupFactor > BLUR_BLOWUP_FACTOR_THRESHOLD.
+ * We set SVG files to the maximum screen size available.
+ * Assumes set function called before.
+ *
+ * @param {{width: number, height: number}} size
+ * @param {jQuery} $imagePlaceholder Image placeholder to be displayed while the real image loads.
+ * @param {mw.mmv.model.ThumbnailWidth} imageWidths
+ * @return {boolean} Whether the image was blured or not
+ */
+ C.maybeDisplayPlaceholder = function ( size, $imagePlaceholder, imageWidths ) {
+ var targetWidth,
+ targetHeight,
+ blowupFactor,
+ blurredThumbnailShown = false;
+
+ // Assume natural thumbnail size¸
+ targetWidth = size.width;
+ targetHeight = size.height;
+
+ // If the image is bigger than the screen we need to resize it
+ if ( size.width > imageWidths.cssWidth ) { // This assumes imageInfo.width in CSS units
+ targetWidth = imageWidths.cssWidth;
+ targetHeight = imageWidths.cssHeight;
+ }
+
+ blowupFactor = targetWidth / $imagePlaceholder.width();
+
+ // If the placeholder is too blown up, it's not worth showing it
+ if ( blowupFactor > Canvas.MAX_BLOWUP_FACTOR ) {
+ return blurredThumbnailShown;
+ }
+
+ $imagePlaceholder.width( targetWidth );
+ $imagePlaceholder.height( targetHeight );
+
+ // Only blur the placeholder if it's blown up significantly
+ if ( blowupFactor > Canvas.BLUR_BLOWUP_FACTOR_THRESHOLD ) {
+ this.blur( $imagePlaceholder );
+ blurredThumbnailShown = true;
+ }
+
+ this.set( this.imageRawMetadata, $imagePlaceholder.show() );
+
+ return blurredThumbnailShown;
+ };
+
+ /**
+ * Blur image
+ *
+ * @param {jQuery} $image Image to be blurred.
+ */
+ C.blur = function ( $image ) {
+ // We have to apply the SVG filter here, it doesn't work when defined in the .less file
+ // We can't use an external SVG file because filters can't be accessed cross-domain
+ // We can't embed the SVG file because accessing the filter inside of it doesn't work
+ $image.addClass( 'blurred' ).css( 'filter', 'url("#gaussian-blur")' );
+ };
+
+ /**
+ * Animates the image into focus
+ */
+ C.unblurWithAnimation = function () {
+ var self = this,
+ animationLength = 300;
+
+ // The blurred class has an opacity < 1. This animated the image to become fully opaque
+ this.$image
+ .addClass( 'blurred' )
+ .animate( { opacity: 1.0 }, animationLength );
+
+ // During the same amount of time (animationLength) we animate a blur value from 3.0 to 0.0
+ // We pass that value to an inline CSS Gaussian blur effect
+ $( { blur: 3.0 } ).animate( { blur: 0.0 }, {
+ duration: animationLength,
+ step: function ( step ) {
+ self.$image.css( { '-webkit-filter': 'blur(' + step + 'px)',
+ filter: 'blur(' + step + 'px)' } );
+ },
+ complete: function () {
+ // When the animation is complete, the blur value is 0, clean things up
+ self.unblur();
+ }
+ } );
+ };
+
+ C.unblur = function () {
+ // We apply empty CSS values to remove the inline styles applied by jQuery
+ // so that they don't get in the way of styles defined in CSS
+ this.$image.css( { '-webkit-filter': '', opacity: '', filter: '' } )
+ .removeClass( 'blurred' );
+ };
+
+ /**
+ * Displays a message and error icon when loading the image fails.
+ *
+ * @param {string} error error message
+ */
+ C.showError = function ( error ) {
+ var errorDetails, description, errorUri, retryLink, reportLink,
+ canvasDimensions = this.getDimensions(),
+ thumbnailDimensions = this.getCurrentImageWidths(),
+ htmlUtils = new mw.mmv.HtmlUtils();
+
+ errorDetails = [
+ 'error: ' + error,
+ 'URL: ' + location.href,
+ 'user agent: ' + navigator.userAgent,
+ 'screen size: ' + screen.width + 'x' + screen.height,
+ 'canvas size: ' + canvasDimensions.width + 'x' + canvasDimensions.height,
+ 'image size: ' + this.imageRawMetadata.originalWidth + 'x' + this.imageRawMetadata.originalHeight,
+ 'thumbnail size: CSS: ' + thumbnailDimensions.cssWidth + 'x' + thumbnailDimensions.cssHeight +
+ ', screen width: ' + thumbnailDimensions.screen + ', real width: ' + thumbnailDimensions.real
+ ];
+ // ** is bolding in Phabricator
+ description = '**' + mw.message( 'multimediaviewer-errorreport-privacywarning' ).text() + '**\n\n\n' +
+ 'Error details:\n\n' + errorDetails.join( '\n' );
+ errorUri = mw.msg( 'multimediaviewer-report-issue-url', encodeURIComponent( description ) );
+
+ retryLink = $( '<a>' ).addClass( 'mw-mmv-retry-link' ).text(
+ mw.msg( 'multimediaviewer-thumbnail-error-retry' ) );
+ reportLink = $( '<a>' ).attr( 'href', errorUri ).text(
+ mw.msg( 'multimediaviewer-thumbnail-error-report' ) );
+
+ this.$imageDiv.empty()
+ .addClass( 'error' )
+ .append(
+ $( '<div>' ).addClass( 'error-box' ).append(
+ $( '<div>' ).addClass( 'mw-mmv-error-text' ).text(
+ mw.msg( 'multimediaviewer-thumbnail-error' )
+ )
+ ).append(
+ $( '<div>' ).addClass( 'mw-mmv-error-description' ).append(
+ mw.msg( 'multimediaviewer-thumbnail-error-description',
+ htmlUtils.jqueryToHtml( retryLink ),
+ error,
+ htmlUtils.jqueryToHtml( reportLink )
+ )
+ )
+ )
+ );
+ this.$imageDiv.find( '.mw-mmv-retry-link' ).click( function () {
+ location.reload();
+ } );
+ };
+
+ /**
+ * Returns width and height of the canvas area (i.e. the space available for the image).
+ *
+ * @param {boolean} forFullscreen if true, return size in fullscreen mode; otherwise, return current size
+ * (which might still be fullscreen mode).
+ * @return {Object} Width and height in CSS pixels
+ */
+ C.getDimensions = function ( forFullscreen ) {
+ var $window = $( window ),
+ $aboveFold = $( '.mw-mmv-above-fold' ),
+ isFullscreened = !!$aboveFold.closest( '.jq-fullscreened' ).length,
+ // Don't rely on this.$imageWrapper's sizing because it's fragile.
+ // Depending on what the wrapper contains, its size can be 0 on some browsers.
+ // Therefore, we calculate the available space manually
+ availableWidth = $window.width(),
+ availableHeight = $window.height() - ( isFullscreened ? 0 : $aboveFold.outerHeight() );
+
+ if ( forFullscreen ) {
+ return {
+ width: screen.width,
+ height: screen.height
+ };
+ } else {
+ return {
+ width: availableWidth,
+ height: availableHeight
+ };
+ }
+ };
+
+ /**
+ * Gets the widths for a given lightbox image.
+ *
+ * @param {mw.mmv.LightboxImage} image
+ * @return {mw.mmv.model.ThumbnailWidth}
+ */
+ C.getLightboxImageWidths = function ( image ) {
+ var thumb = image.thumbnail,
+ canvasDimensions = this.getDimensions();
+
+ return this.thumbnailWidthCalculator.calculateWidths(
+ canvasDimensions.width, canvasDimensions.height, thumb.width, thumb.height );
+ };
+
+ /**
+ * Gets the fullscreen widths for a given lightbox image.
+ * Intended for use before the viewer is in fullscreen mode
+ * (in fullscreen mode getLightboxImageWidths() works fine).
+ *
+ * @param {mw.mmv.LightboxImage} image
+ * @return {mw.mmv.model.ThumbnailWidth}
+ */
+ C.getLightboxImageWidthsForFullscreen = function ( image ) {
+ var thumb = image.thumbnail,
+ canvasDimensions = this.getDimensions( true );
+
+ return this.thumbnailWidthCalculator.calculateWidths(
+ canvasDimensions.width, canvasDimensions.height, thumb.width, thumb.height );
+ };
+
+ /**
+ * Gets the widths for the current lightbox image.
+ *
+ * @return {mw.mmv.model.ThumbnailWidth}
+ */
+ C.getCurrentImageWidths = function () {
+ return this.getLightboxImageWidths( this.imageRawMetadata );
+ };
+
+ mw.mmv.ui.Canvas = Canvas;
+}( mediaWiki, jQuery, OO ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.canvas.less b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.canvas.less
new file mode 100644
index 00000000..63ff35f1
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.canvas.less
@@ -0,0 +1,89 @@
+@import '../mmv.mixins';
+
+.mw-mmv-image {
+ display: table-cell;
+ width: 100%;
+ height: 100%;
+ vertical-align: middle;
+ .unselectable;
+
+ &.empty {
+ display: none;
+ }
+ &.error {
+ background-color: #222;
+
+ a,
+ a:visited {
+ cursor: pointer;
+ color: #3472e5;
+ }
+ }
+
+ .error-box {
+ /* @embed */
+ background: url( img/error-media-icon.svg ) no-repeat 0 0;
+ background-size: 110px 110px;
+
+ position: absolute;
+ left: 50%;
+ margin-left: -350px;
+ top: 50%;
+ margin-top: -100px;
+
+ padding: 0 20px 0 160px;
+ color: #fff;
+
+ max-width: 520px;
+
+ .mw-mmv-error-text {
+ font-size: 48px;
+ }
+
+ .mw-mmv-error-description {
+ margin-top: 30px;
+ font-size: 22px;
+ }
+ }
+}
+
+.mw-mmv-image img {
+ display: block;
+ margin-right: auto;
+ margin-left: auto;
+ cursor: pointer;
+ cursor: zoom-in;
+
+ &.blurred {
+ filter: blur( 3px );
+ -webkit-filter: blur( 3px );
+ .opacity( 0.8 );
+ }
+
+ /* Whitelist file types that are potentially transparent.
+ We don't set it for other file types because Media Viewer plugins
+ can find that undesirable (eg. 3d) */
+ &.gif,
+ &.png,
+ &.svg,
+ &.tiff,
+ &.tif {
+ background: url( checker.png ) repeat;
+ }
+
+ &.mw-mmv-dialog-is-open {
+ cursor: default;
+ }
+
+ .metadata-panel-is-open & {
+ cursor: pointer;
+ }
+}
+
+.mw-mmv-image.empty img {
+ display: none;
+}
+
+.metadata-panel-is-open .mw-mmv-image-wrapper {
+ cursor: pointer;
+}
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.canvasButtons.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.canvasButtons.js
new file mode 100644
index 00000000..0bf6c2c6
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.canvasButtons.js
@@ -0,0 +1,285 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $, oo ) {
+ var CBP;
+
+ /**
+ * Represents the buttons which are displayed over the image - next, previous, close
+ * and fullscreen.
+ *
+ * @class mw.mmv.ui.CanvasButtons
+ * @extends mw.mmv.ui.Element
+ * @constructor
+ * @param {jQuery} $container The parent element we should put the buttons into.
+ * @param {jQuery} $closeButton The close button element from the parent class.
+ * @param {jQuery} $fullscreenButton The fullscreen button from the parent class.
+ */
+ function CanvasButtons( $container, $closeButton, $fullscreenButton ) {
+ var buttons = this,
+ tooltipDelay = mw.config.get( 'wgMultimediaViewer' ).tooltipDelay;
+
+ mw.mmv.ui.Element.call( this, $container );
+
+ this.$close = $closeButton;
+ this.$fullscreen = $fullscreenButton;
+
+ this.$reuse = $( '<button>' )
+ .addClass( 'mw-mmv-reuse-button' )
+ .html( '&nbsp;' )
+ .prop( 'title', mw.message( 'multimediaviewer-reuse-link' ).text() )
+ .tipsy( {
+ delayIn: tooltipDelay,
+ gravity: this.correctEW( 'se' )
+ } );
+
+ this.$options = $( '<button>' )
+ .text( ' ' )
+ .prop( 'title', mw.message( 'multimediaviewer-options-tooltip' ).text() )
+ .addClass( 'mw-mmv-options-button' )
+ .tipsy( {
+ delayIn: tooltipDelay,
+ gravity: this.correctEW( 'se' )
+ } );
+
+ this.$download = $( '<button>' )
+ .addClass( 'mw-mmv-download-button' )
+ .html( '&nbsp;' )
+ .prop( 'title', mw.message( 'multimediaviewer-download-link' ).text() )
+ .tipsy( {
+ delayIn: tooltipDelay,
+ gravity: this.correctEW( 'se' )
+ } );
+
+ this.$next = $( '<button>' )
+ .prop( 'title', mw.message( 'multimediaviewer-next-image-alt-text' ).text() )
+ .addClass( 'mw-mmv-next-image disabled' )
+ .html( '&nbsp;' );
+
+ this.$prev = $( '<button>' )
+ .prop( 'title', mw.message( 'multimediaviewer-prev-image-alt-text' ).text() )
+ .addClass( 'mw-mmv-prev-image disabled' )
+ .html( '&nbsp;' );
+
+ this.$nav = this.$next
+ .add( this.$prev );
+
+ this.$buttons = this.$close
+ .add( this.$download )
+ .add( this.$reuse )
+ .add( this.$fullscreen )
+ .add( this.$options )
+ .add( this.$next )
+ .add( this.$prev );
+
+ this.$buttons.appendTo( this.$container );
+
+ $( document ).on( 'mmv-close', function () {
+ buttons.$nav.addClass( 'disabled' );
+ } );
+
+ this.$close.click( function () {
+ $container.trigger( $.Event( 'mmv-close' ) );
+ } );
+
+ this.$next.click( function () {
+ buttons.emit( 'next' );
+ } );
+
+ this.$prev.click( function () {
+ buttons.emit( 'prev' );
+ } );
+ }
+ oo.inheritClass( CanvasButtons, mw.mmv.ui.Element );
+ CBP = CanvasButtons.prototype;
+
+ /**
+ * Sets the top offset for the navigation buttons.
+ *
+ * @param {number} offset
+ */
+ CBP.setOffset = function ( offset ) {
+ this.$nav.css( {
+ top: offset
+ } );
+ };
+
+ /**
+ * Stops the fading animation of the buttons and cancel any opacity value
+ */
+ CBP.stopFade = function () {
+ this.$buttons
+ .stop( true )
+ .removeClass( 'hidden' )
+ .css( 'opacity', '' );
+
+ this.$container.trigger( $.Event( 'mmv-fade-stopped' ) );
+ };
+
+ /**
+ * Toggles buttons being disabled or not
+ *
+ * @param {boolean} showPrevButton
+ * @param {boolean} showNextButton
+ */
+ CBP.toggle = function ( showPrevButton, showNextButton ) {
+ this.$next.toggleClass( 'disabled', !showPrevButton );
+ this.$prev.toggleClass( 'disabled', !showNextButton );
+ };
+
+ /**
+ * Fades out the active buttons
+ */
+ CBP.fadeOut = function () {
+ var buttons = this;
+
+ // We don't use animation chaining because delay() can't be stop()ed
+ this.buttonsFadeTimeout = setTimeout( function () {
+ buttons.$buttons.not( '.disabled' ).animate( { opacity: 0 }, 1000, 'swing',
+ function () {
+ buttons.$buttons.addClass( 'hidden' );
+ buttons.$container.trigger( $.Event( 'mmv-faded-out' ) );
+ } );
+ }, 1500 );
+ };
+
+ /**
+ * Checks if any active buttons are currently hovered, given a position
+ *
+ * @param {number} x The horizontal coordinate of the position
+ * @param {number} y The vertical coordinate of the position
+ * @return {boolean}
+ */
+ CBP.isAnyActiveButtonHovered = function ( x, y ) {
+ // We don't use mouseenter/mouseleave events because content is subject
+ // to change underneath the cursor, eg. when entering fullscreen or
+ // when going prev/next (the button can disappear when reaching ends)
+ var hovered = false;
+
+ this.$buttons.not( '.disabled' ).each( function ( idx, e ) {
+ var $e = $( e ),
+ offset = $e.offset();
+
+ if ( y >= offset.top &&
+ // using css( 'height' ) & css( 'width' ) instead of .height()
+ // and .width() since those don't include padding, and as a
+ // result can return a smaller size than is actually the button
+ y <= offset.top + parseInt( $e.css( 'height' ) ) &&
+ x >= offset.left &&
+ x <= offset.left + parseInt( $e.css( 'width' ) ) ) {
+ hovered = true;
+ }
+ } );
+
+ return hovered;
+ };
+
+ /**
+ * Reveals all active buttons and schedule a fade out if needed
+ *
+ * @param {Object} [mousePosition] Mouse position containing 'x' and 'y' properties
+ */
+ CBP.revealAndFade = function ( mousePosition ) {
+ if ( this.buttonsFadeTimeout ) {
+ clearTimeout( this.buttonsFadeTimeout );
+ }
+
+ // Stop ongoing animations and make sure the buttons that need to be displayed are displayed
+ this.stopFade();
+
+ // mousePosition can be empty, for instance when we enter fullscreen and haven't
+ // recorded a real mousemove event yet
+ if ( !mousePosition ||
+ !this.isAnyActiveButtonHovered( mousePosition.x, mousePosition.y ) ) {
+ this.fadeOut();
+ }
+ };
+
+ /**
+ * @event mmv-reuse-open
+ * Fired when the button to open the reuse dialog is clicked.
+ */
+ /**
+ * Registers listeners.
+ */
+ CBP.attach = function () {
+ var buttons = this;
+
+ this.$reuse.on( 'click.mmv-canvasButtons', function ( e ) {
+ $( document ).trigger( 'mmv-reuse-open', e );
+ e.stopPropagation(); // the dialog would take it as an outside click and close
+ } );
+ this.handleEvent( 'mmv-reuse-opened', function () {
+ buttons.$reuse.addClass( 'open' );
+ } );
+ this.handleEvent( 'mmv-reuse-closed', function () {
+ buttons.$reuse.removeClass( 'open' );
+ } );
+
+ this.$download.on( 'click.mmv-canvasButtons', function ( e ) {
+ $( document ).trigger( 'mmv-download-open', e );
+ e.stopPropagation();
+ } );
+ this.handleEvent( 'mmv-download-opened', function () {
+ buttons.$download.addClass( 'open' );
+ } );
+ this.handleEvent( 'mmv-download-closed', function () {
+ buttons.$download.removeClass( 'open' );
+ } );
+
+ this.$options.on( 'click.mmv-canvasButtons', function ( e ) {
+ $( document ).trigger( 'mmv-options-open', e );
+ e.stopPropagation();
+ } );
+ this.handleEvent( 'mmv-options-opened', function () {
+ buttons.$options.addClass( 'open' );
+ } );
+ this.handleEvent( 'mmv-options-closed', function () {
+ buttons.$options.removeClass( 'open' );
+ } );
+
+ this.$download
+ .add( this.$reuse )
+ .add( this.$options )
+ .add( this.$close )
+ .add( this.$fullscreen )
+ .each( function () {
+ $( this ).tipsy( 'enable' );
+ } );
+ };
+
+ /**
+ * Removes all UI things from the DOM, or hides them
+ */
+ CBP.unattach = function () {
+ this.$download
+ .add( this.$reuse )
+ .add( this.$options )
+ .add( this.$close )
+ .add( this.$fullscreen )
+ .off( 'click.mmv-canvasButtons' )
+ .each( function () {
+ $( this ).tipsy( 'hide' ).tipsy( 'disable' );
+ } );
+ };
+
+ CBP.empty = function () {
+ this.$reuse.removeClass( 'open' );
+ };
+
+ mw.mmv.ui.CanvasButtons = CanvasButtons;
+}( mediaWiki, jQuery, OO ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.canvasButtons.less b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.canvasButtons.less
new file mode 100644
index 00000000..bd73beb4
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.canvasButtons.less
@@ -0,0 +1,142 @@
+@import '../mmv.globals';
+@import '../mmv.mixins';
+
+.mw-mmv-download-button,
+.mw-mmv-reuse-button,
+.mw-mmv-options-button,
+.mw-mmv-close,
+.mw-mmv-fullscreen,
+.mw-mmv-next-image,
+.mw-mmv-prev-image {
+ cursor: pointer;
+ position: fixed;
+ background-color: transparent;
+ background-repeat: no-repeat;
+ .opacity( 0.8 );
+ border: 0;
+ z-index: 1003;
+
+ &.mw-mmv-dialog-open,
+ &:hover {
+ .opacity( 1 );
+ }
+
+ .unselectable;
+}
+
+.mw-mmv-download-button.hidden,
+.mw-mmv-reuse-button.hidden,
+.mw-mmv-options-button.hidden,
+.mw-mmv-close.hidden,
+.mw-mmv-fullscreen.hidden,
+.mw-mmv-next-image.hidden,
+.mw-mmv-prev-image.hidden {
+ display: none;
+}
+
+.cursor-hidden {
+ .mw-mmv-download-button,
+ .mw-mmv-reuse-button,
+ .mw-mmv-close,
+ .mw-mmv-fullscreen,
+ .mw-mmv-next-image,
+ .mw-mmv-prev-image {
+ cursor: none;
+ }
+}
+
+.mw-mmv-download-button,
+.mw-mmv-reuse-button,
+.mw-mmv-options-button,
+.mw-mmv-close,
+.mw-mmv-fullscreen {
+ right: @buttons-offset-right;
+ left: auto;
+ transition: opacity 0.25s;
+ background-position: center;
+ margin-top: 14px;
+ margin-right: 14px;
+}
+
+.mw-mmv-next-image,
+.mw-mmv-prev-image {
+ top: -999px;
+ width: 80px;
+ height: 120px;
+ transition: opacity 0.25s, margin 0.25s;
+
+ &.disabled {
+ display: none;
+ cursor: none;
+ }
+}
+
+.mw-mmv-close {
+ top: @buttons-offset-right;
+ /* @embed */
+ background-image: url( img/mw-close.svg );
+ height: 23px;
+ width: 23px;
+}
+
+.mw-mmv-fullscreen {
+ top: ( @buttons-offset-right + ( @buttons-offset-each-top ) );
+ /* @embed */
+ background-image: url( img/mw-fullscreen-ltr.svg );
+ width: 21px;
+ height: 22px;
+}
+
+.mw-mmv-options-button {
+ top: ( @buttons-offset-right + ( 2 * @buttons-offset-each-top ) );
+ /* @embed */
+ background-image: url( img/gear.svg );
+ height: 23px;
+ width: 23px;
+}
+
+.jq-fullscreened {
+ .mw-mmv-fullscreen {
+ /* @embed */
+ background-image: url( img/mw-defullscreen-ltr.svg );
+ }
+}
+
+.mw-mmv-next-image {
+ /* @embed */
+ background-image: url( img/next-ltr.svg );
+ background-position: right;
+ right: @navbutton-width;
+
+ &:hover {
+ margin-right: -4px;
+ }
+}
+
+.mw-mmv-prev-image {
+ /* @embed */
+ background-image: url( img/prev-ltr.svg );
+ background-position: left;
+ left: @navbutton-width;
+
+ &:hover {
+ margin-left: -4px;
+ }
+}
+
+.mw-mmv-reuse-button {
+ right: @buttons-offset-right - 2px;
+ bottom: @buttons-offset-right + @metadatabar-above-fold-height + @progress-bar-height;
+ /* @embed */
+ background-image: url( img/use-ltr.svg );
+ height: 24px;
+ width: 24px;
+}
+
+.mw-mmv-download-button {
+ bottom: @buttons-offset-right + @metadatabar-above-fold-height + @progress-bar-height + 37px;
+ /* @embed */
+ background-image: url( img/mw-download.svg );
+ height: 24px;
+ width: 24px;
+}
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.description.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.description.js
new file mode 100644
index 00000000..558877c8
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.description.js
@@ -0,0 +1,69 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $, oo ) {
+ /**
+ * Description element in the UI.
+ *
+ * @class mw.mmv.ui.Description
+ * @extends mw.mmv.ui.Element
+ * @constructor
+ * @inheritdoc
+ */
+ function Description( $container ) {
+ mw.mmv.ui.Element.call( this, $container );
+
+ /** @property {mw.mmv.HtmlUtils} htmlUtils - */
+ this.htmlUtils = new mw.mmv.HtmlUtils();
+
+ this.$imageDescDiv = $( '<div>' )
+ .addClass( 'mw-mmv-image-desc-div empty' )
+ .appendTo( this.$container );
+
+ this.$imageDesc = $( '<p>' )
+ .addClass( 'mw-mmv-image-desc' )
+ .appendTo( this.$imageDescDiv );
+ }
+
+ oo.inheritClass( Description, mw.mmv.ui.Element );
+
+ /**
+ * Sets data on the element.
+ * This complements MetadataPanel.setTitle() - information shown there will not be shown here.
+ *
+ * @param {string|null} description The text of the description
+ * @param {string|null} caption The text of the caption
+ */
+ Description.prototype.set = function ( description, caption ) {
+ if ( caption && description ) { // panel header shows the caption - show description here
+ this.$imageDesc.html( this.htmlUtils.htmlToTextWithTags( description ) );
+ this.$imageDescDiv.removeClass( 'empty' );
+ } else { // either there is no description or the paner header already shows it - nothing to do here
+ this.empty();
+ }
+ };
+
+ /**
+ * @inheritdoc
+ */
+ Description.prototype.empty = function () {
+ this.$imageDesc.empty();
+ this.$imageDescDiv.addClass( 'empty' );
+ };
+
+ mw.mmv.ui.Description = Description;
+}( mediaWiki, jQuery, OO ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.dialog.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.dialog.js
new file mode 100644
index 00000000..6738b50b
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.dialog.js
@@ -0,0 +1,258 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $, oo ) {
+ // Shortcut for prototype later
+ var DP;
+
+ /**
+ * Represents a dialog and the link to open it.
+ *
+ * @class mw.mmv.ui.Dialog
+ * @extends mw.mmv.ui.Element
+ * @param {jQuery} $container the element to which the dialog will be appended
+ * @param {jQuery} $openButton the button which opens the dialog. Only used for positioning.
+ * @param {mw.mmv.Config} config
+ */
+ function Dialog( $container, $openButton, config ) {
+ mw.mmv.ui.Element.call( this, $container );
+
+ /** @property {boolean} isOpen Whether or not the dialog is open. */
+ this.isOpen = false;
+
+ /**
+ * @property {string[]} loadDependencies Dependencies to load before showing the dialog.
+ */
+ this.loadDependencies = [];
+
+ /**
+ * @property {string} eventPrefix Prefix specific to the class to be applied to events.
+ */
+ this.eventPrefix = '';
+ /** @property {mw.mmv.Config} config - */
+ this.config = config;
+
+ /** @property {jQuery} $openButton The click target which opens the dialog. */
+ this.$openButton = $openButton;
+
+ /** @type {jQuery} $dialog The main dialog container */
+ this.$dialog = $( '<div>' )
+ .addClass( 'mw-mmv-dialog' );
+
+ /**
+ * @property {jQuery} $downArrow Tip of the dialog pointing to $openButton. Called
+ * downArrow for historical reasons although it does not point down anymore.
+ */
+ this.$downArrow = $( '<div>' )
+ .addClass( 'mw-mmv-dialog-down-arrow' )
+ .appendTo( this.$dialog );
+
+ this.initWarning();
+
+ this.$dialog.appendTo( this.$container );
+ }
+
+ oo.inheritClass( Dialog, mw.mmv.ui.Element );
+ DP = Dialog.prototype;
+
+ /**
+ * Creates the DOM element that setWarning()/clearWarning() will operate on.
+ * @private
+ */
+ DP.initWarning = function () {
+ this.$warning = $( '<div>' )
+ .addClass( 'mw-mmv-dialog-warning' )
+ .hide()
+ .click( function ( e ) {
+ // prevent other click handlers such as the download CTA from intercepting clicks at the warning
+ e.stopPropagation();
+ } )
+ .appendTo( this.$dialog );
+ };
+
+ /**
+ * Handles click on link that opens/closes the dialog.
+ *
+ * @param {jQuery.Event} openEvent Event object for the mmv-$dialog-open event.
+ * @param {jQuery.Event} e Event object for the click event.
+ * @return {boolean} False to cancel the default event
+ */
+ DP.handleOpenCloseClick = function ( openEvent, e ) {
+ var dialog = this;
+
+ mw.loader.using( this.loadDependencies, function () {
+ dialog.dependenciesLoaded = true;
+ dialog.toggleDialog( e );
+ }, function ( error ) {
+ if ( window.console && window.console.error ) {
+ window.console.error( 'mw.loader.using error when trying to load dialog dependencies', error );
+ }
+ } );
+
+ return false;
+ };
+
+ /**
+ * Toggles the open state on the dialog.
+ *
+ * @param {jQuery.Event} [e] Event object when the close action is caused by a user
+ * action, as opposed to closing the window or something.
+ */
+ DP.toggleDialog = function ( e ) {
+ if ( this.isOpen ) {
+ this.closeDialog( e );
+ } else {
+ this.openDialog();
+ }
+ };
+
+ /**
+ * Opens a dialog.
+ */
+ DP.openDialog = function () {
+ mw.mmv.actionLogger.log( this.eventPrefix + '-open' );
+
+ this.startListeningToOutsideClick();
+ this.$dialog.show();
+ this.isOpen = true;
+ this.$openButton.addClass( 'mw-mmv-dialog-open' );
+ };
+
+ /**
+ * Closes a dialog.
+ */
+ DP.closeDialog = function () {
+ if ( this.isOpen ) {
+ mw.mmv.actionLogger.log( this.eventPrefix + '-close' );
+ }
+
+ this.stopListeningToOutsideClick();
+ this.$dialog.hide();
+ this.isOpen = false;
+ this.$openButton.removeClass( 'mw-mmv-dialog-open' );
+ };
+
+ /**
+ * Sets up the event handler which closes the dialog when the user clicks outside.
+ */
+ DP.startListeningToOutsideClick = function () {
+ var dialog = this;
+
+ this.outsideClickHandler = this.outsideClickHandler || function ( e ) {
+ var $clickTarget = $( e.target );
+
+ // Don't close the dialog if the click inside a dialog or on an navigation arrow
+ if (
+ $clickTarget.closest( dialog.$dialog ).length ||
+ $clickTarget.closest( '.mw-mmv-next-image' ).length ||
+ $clickTarget.closest( '.mw-mmv-prev-image' ).length ||
+ e.which === 3
+ ) {
+ return;
+ }
+
+ dialog.closeDialog();
+ return false;
+ };
+ $( document ).on( 'click.mmv.' + this.eventPrefix, this.outsideClickHandler );
+ };
+
+ /**
+ * Removes the event handler set up by startListeningToOutsideClick().
+ */
+ DP.stopListeningToOutsideClick = function () {
+ $( document ).off( 'click.mmv.' + this.eventPrefix, this.outsideClickHandler );
+ };
+
+ /**
+ * Clears listeners.
+ */
+ DP.unattach = function () {
+ mw.mmv.ui.Element.prototype.unattach.call( this );
+
+ this.stopListeningToOutsideClick();
+ };
+
+ /**
+ * @inheritdoc
+ */
+ DP.empty = function () {
+ this.closeDialog();
+ this.clearWarning();
+ };
+
+ /**
+ * Displays a warning ribbon.
+ * @param {string} content Content of the warning (can be HTML,
+ * setWarning does no escaping).
+ */
+ DP.setWarning = function ( content ) {
+ this.$warning
+ .empty()
+ .append( content )
+ .show();
+ this.$dialog.addClass( 'mw-mmv-warning-visible' );
+ };
+
+ /**
+ * Removes the warning ribbon.
+ */
+ DP.clearWarning = function () {
+ this.$warning.hide();
+ this.$dialog.removeClass( 'mw-mmv-warning-visible' );
+ };
+
+ /**
+ * @param {mw.mmv.model.Image} image
+ * @return {string[]}
+ */
+ DP.getImageWarnings = function ( image ) {
+ var warnings = [];
+
+ if ( image.deletionReason ) {
+ warnings.push( mw.message( 'multimediaviewer-reuse-warning-deletion' ).plain() );
+ // Don't inform about other warnings (they may be the cause of the deletion)
+ return warnings;
+ }
+
+ if ( !image.license || image.license.needsAttribution() && !image.author && !image.attribution ) {
+ warnings.push( mw.message( 'multimediaviewer-reuse-warning-noattribution' ).plain() );
+ }
+
+ if ( image.license && !image.license.isFree() ) {
+ warnings.push( mw.message( 'multimediaviewer-reuse-warning-nonfree' ).plain() );
+ }
+
+ return warnings;
+ };
+
+ /**
+ * @param {mw.mmv.model.Image} image
+ */
+ DP.showImageWarnings = function ( image ) {
+ var warnings = this.getImageWarnings( image );
+
+ if ( warnings.length > 0 ) {
+ warnings.push( mw.message( 'multimediaviewer-reuse-warning-generic', image.descriptionUrl ).parse() );
+ this.setWarning( warnings.join( '<br />' ) );
+ } else {
+ this.clearWarning();
+ }
+ };
+
+ mw.mmv.ui.Dialog = Dialog;
+}( mediaWiki, jQuery, OO ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.dialog.less b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.dialog.less
new file mode 100644
index 00000000..cccbfac3
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.dialog.less
@@ -0,0 +1,45 @@
+@import '../mmv.mixins';
+@import '../mmv.globals';
+
+@background-color: #fff;
+@dialog-width: 450px;
+@box-shadow-dialog: 0 2px 2px 0 darken( @background-color, 33.3% );
+
+.mw-mmv-dialog {
+ position: fixed;
+ right: 58px;
+ display: none;
+ width: @dialog-width;
+ height: @dialog-height;
+ background-color: @background-color;
+ box-shadow: @box-shadow-dialog;
+ border-radius: @border-radius;
+ z-index: 1004;
+
+ .mw-mmv-dialog-down-arrow {
+ right: 48px;
+ background-color: @background-color;
+ width: @arrow-size;
+ height: @arrow-size;
+ .rotate( -45deg );
+ position: fixed;
+ }
+
+ .mw-mmv-dialog-copy {
+ /* @embed */
+ background-image: url( img/pasting.svg );
+ background-size: contain;
+ background-position: right center;
+ background-repeat: no-repeat;
+
+ &:hover {
+ /* @embed */
+ background-image: url( img/pasting-hover.svg );
+ text-decoration: none;
+ }
+ }
+
+ .mw-mmv-dialog-warning {
+ background-color: @dialog-warning-color;
+ }
+}
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.download.dialog.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.download.dialog.js
new file mode 100644
index 00000000..d0c1847d
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.download.dialog.js
@@ -0,0 +1,128 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $, oo ) {
+ // Shortcut for prototype later
+ var DP;
+
+ /**
+ * Represents the file download dialog and the link to open it.
+ *
+ * @class mw.mmv.ui.download.Dialog
+ * @extends mw.mmv.ui.Dialog
+ * @param {jQuery} $container the element to which the dialog will be appended
+ * @param {jQuery} $openButton the button which opens the dialog. Only used for positioning.
+ * @param {mw.mmv.Config} config
+ */
+ function Dialog( $container, $openButton, config ) {
+ mw.mmv.ui.Dialog.call( this, $container, $openButton, config );
+
+ this.loadDependencies.push( 'mmv.ui.download.pane' );
+
+ this.$dialog.addClass( 'mw-mmv-download-dialog' );
+
+ this.eventPrefix = 'download';
+ }
+
+ oo.inheritClass( Dialog, mw.mmv.ui.Dialog );
+ DP = Dialog.prototype;
+
+ /**
+ * Registers listeners.
+ */
+ DP.attach = function () {
+ var dialog = this;
+
+ this.handleEvent( 'mmv-download-open', $.proxy( this.handleOpenCloseClick, this ) );
+
+ this.handleEvent( 'mmv-reuse-open', $.proxy( this.closeDialog, this ) );
+ this.handleEvent( 'mmv-options-open', $.proxy( this.closeDialog, this ) );
+
+ this.$container.on( 'mmv-download-cta-open', function () {
+ dialog.$warning.hide();
+ } );
+ this.$container.on( 'mmv-download-cta-close', function () {
+ if ( dialog.$dialog.hasClass( 'mw-mmv-warning-visible' ) ) {
+ dialog.$warning.show();
+ }
+ } );
+ };
+
+ /**
+ * Clears listeners.
+ */
+ DP.unattach = function () {
+ this.$container.off( 'mmv-download-cta-open mmv-download-cta-close' );
+ };
+
+ /**
+ * Sets data needed by contaned tabs and makes dialog launch link visible.
+ *
+ * @param {mw.mmv.model.Image} image
+ * @param {mw.mmv.model.Repo} repo
+ */
+ DP.set = function ( image, repo ) {
+ if ( this.download ) {
+ this.download.set( image, repo );
+ this.showImageWarnings( image );
+ } else {
+ this.setValues = {
+ image: image,
+ repo: repo
+ };
+ }
+ };
+
+ /**
+ * @event mmv-download-opened
+ * Fired when the dialog is opened.
+ */
+ /**
+ * Opens a dialog with information about file download.
+ */
+ DP.openDialog = function () {
+ if ( !this.download ) {
+ this.download = new mw.mmv.ui.download.Pane( this.$dialog );
+ this.download.attach();
+ }
+
+ if ( this.setValues ) {
+ this.download.set( this.setValues.image, this.setValues.repo );
+ this.showImageWarnings( this.setValues.image );
+ this.setValues = undefined;
+ }
+
+ mw.mmv.ui.Dialog.prototype.openDialog.call( this );
+
+ $( document ).trigger( 'mmv-download-opened' );
+ };
+
+ /**
+ * @event mmv-download-closed
+ * Fired when the dialog is closed.
+ */
+ /**
+ * Closes the download dialog.
+ */
+ DP.closeDialog = function () {
+ mw.mmv.ui.Dialog.prototype.closeDialog.call( this );
+
+ $( document ).trigger( 'mmv-download-closed' );
+ };
+
+ mw.mmv.ui.download.Dialog = Dialog;
+}( mediaWiki, jQuery, OO ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.download.dialog.less b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.download.dialog.less
new file mode 100644
index 00000000..d4615a17
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.download.dialog.less
@@ -0,0 +1,34 @@
+@import '../mmv.mixins';
+@import '../mmv.globals';
+
+.mw-mmv-download-dialog {
+ // resetting height (to overwrite .mw-mmv-dialog's bigger height), since the
+ // height may have to change for warnings being added
+ height: initial;
+ // positioned relative to the download button
+ position: fixed;
+ bottom: @metadatabar-above-fold-height + @progress-bar-height + 35px;
+
+ .mw-mmv-download-size .oo-ui-optionWidget {
+ display: inline-block;
+ padding: 10px 25px;
+ font-size: 16px;
+
+ &.oo-ui-optionWidget-selected {
+ border: 0;
+ }
+
+ &:first-child {
+ border-radius: @border-radius 0 0 0;
+ }
+ }
+
+ .mw-mmv-dialog-down-arrow {
+ bottom: @metadatabar-above-fold-height + @progress-bar-height + 45px;
+ background-color: #f8f9fa;
+ }
+
+ .mw-mmv-dialog-warning {
+ padding: 10px 30px;
+ }
+}
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.download.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.download.js
new file mode 100644
index 00000000..58c5d269
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.download.js
@@ -0,0 +1,20 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw ) {
+ mw.mmv.ui.download = {};
+}( mediaWiki ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.download.pane.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.download.pane.js
new file mode 100644
index 00000000..560695bc
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.download.pane.js
@@ -0,0 +1,429 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $, oo ) {
+ // Shortcut for prototype later
+ var DP;
+
+ /**
+ * UI component that provides functionality to download the media asset displayed.
+ *
+ * @class mw.mmv.ui.download.Pane
+ * @extends mw.mmv.ui.Element
+ * @constructor
+ * @param {jQuery} $container
+ */
+ function Pane( $container ) {
+ mw.mmv.ui.Element.call( this, $container );
+
+ /** @property {mw.mmv.ui.Utils} utils - */
+ this.utils = new mw.mmv.ui.Utils();
+
+ this.$pane = $( '<div>' )
+ .addClass( 'mw-mmv-download-pane' )
+ .appendTo( this.$container );
+
+ this.$downloadArea = $( '<div>' )
+ .addClass( 'mw-mmv-download-area' )
+ .appendTo( this.$pane );
+
+ this.createDownloadButton( this.$downloadArea );
+ this.createSizePulldownMenu( this.$downloadArea );
+ this.createPreviewLink( this.$downloadArea );
+
+ this.formatter = new mw.mmv.EmbedFileFormatter();
+ this.currentAttrView = 'plain';
+ this.createAttributionButton( this.$pane );
+
+ /**
+ * Default item for the size menu.
+ * @property {OO.ui.MenuOptionWidget}
+ */
+ this.defaultItem = this.downloadSizeMenu.getMenu().findSelectedItem();
+
+ /** @property {mw.mmv.model.Image|null} Image the download button currently points to. */
+ this.image = null;
+ }
+ oo.inheritClass( Pane, mw.mmv.ui.Element );
+ DP = Pane.prototype;
+
+ /**
+ * @event mmv-download-cta-open
+ * Fired when the attribution call to action panel is clicked.
+ */
+ /**
+ * @event mmv-download-cta-close
+ * Fired when the attribution area is closed.
+ */
+
+ /**
+ * Creates download split button. It is a link with the "download" property set plus an
+ * arrow that allows the user to select the image size desired. The "download" property
+ * triggers native browser downloading in browsers that support it. The fallback is the
+ * 'download' parameter which instructs the server to send the right headers so the browser
+ * downloads the file instead of just displaying it. If all this fails, the image will appear
+ * in another window/tab.
+ *
+ * @param {jQuery} $container
+ */
+ DP.createDownloadButton = function ( $container ) {
+ // TODO: Use OOUI progressive button widget instead
+ this.$downloadButton = $( '<a>' )
+ .attr( 'target', '_blank' )
+ .attr( 'download', '' )
+ .addClass( 'mw-ui-button mw-ui-progressive mw-mmv-download-go-button' )
+ .click( function () {
+ mw.mmv.actionLogger.log( 'download' );
+ } );
+
+ this.$selectionArrow = $( '<span>' )
+ .addClass( 'mw-ui-button mw-ui-progressive mw-mmv-download-select-menu' )
+ .append(
+ $( '<span>' )
+ .addClass( 'mw-mmv-download-image-size-name' )
+ .html( '&nbsp;' )
+ )
+ .append(
+ $( '<span>' )
+ .addClass( 'mw-mmv-download-image-size' )
+ .html( '&nbsp;' )
+ );
+
+ $container
+ .append( this.$downloadButton )
+ .append( this.$selectionArrow );
+ };
+
+ /**
+ * Creates pulldown menu to select image sizes.
+ *
+ * @param {jQuery} $container
+ */
+ DP.createSizePulldownMenu = function ( $container ) {
+ this.downloadSizeMenu = this.utils.createPulldownMenu(
+ [ 'original', 'small', 'medium', 'large' ],
+ [ 'mw-mmv-download-size' ],
+ 'original'
+ );
+
+ this.downloadSizeMenu.getMenu().on( 'select', function ( item ) {
+ mw.mmv.actionLogger.log( 'download-select-menu-' + item.data.name );
+ } );
+
+ $container.append( this.downloadSizeMenu.$element );
+ };
+
+ /**
+ * Creates preview link.
+ *
+ * @param {jQuery} $container
+ */
+ DP.createPreviewLink = function ( $container ) {
+ this.$previewLink = $( '<a>' )
+ .attr( 'target', '_blank' )
+ .addClass( 'mw-mmv-download-preview-link' )
+ .text( mw.message( 'multimediaviewer-download-preview-link-title' ).text() )
+ .appendTo( $container )
+ .click( function () {
+ mw.mmv.actionLogger.log( 'download-view-in-browser' );
+ } );
+ };
+
+ DP.createAttributionButton = function ( $container ) {
+ var dl = this,
+ attributionInput = new oo.ui.TextInputWidget( {
+ classes: [ 'mw-mmv-download-attr-input' ],
+ readOnly: true
+ } ),
+ attributionSwitch = new oo.ui.ButtonSelectWidget( {
+ classes: [ 'mw-mmv-download-attr-select' ]
+ } ),
+ plainOption = new oo.ui.ButtonOptionWidget( {
+ data: 'plain',
+ label: mw.message( 'multimediaviewer-attr-plain' ).text()
+ } ),
+ htmlOption = new oo.ui.ButtonOptionWidget( {
+ data: 'html',
+ label: mw.message( 'multimediaviewer-attr-html' ).text()
+ } );
+
+ attributionSwitch.addItems( [
+ plainOption,
+ htmlOption
+ ] );
+
+ attributionSwitch.selectItem( plainOption );
+
+ attributionSwitch.on( 'select', function ( selection ) {
+ dl.selectAttribution( selection.getData() );
+
+ dl.attributionInput.$element.find( 'input' ).focus();
+ } );
+
+ this.$attributionSection = $( '<div>' )
+ .addClass( 'mw-mmv-download-attribution mw-mmv-download-attribution-collapsed' )
+ .appendTo( $container )
+ .click( function () {
+ if ( dl.$attributionSection.hasClass( 'mw-mmv-download-attribution-collapsed' ) ) {
+ dl.$container.trigger( 'mmv-download-cta-open' );
+ dl.$attributionSection.removeClass( 'mw-mmv-download-attribution-collapsed' );
+ dl.attributionInput.$element.find( 'input' ).focus();
+ }
+ } );
+
+ this.$attributionCtaHeader = $( '<p>' )
+ .addClass( 'mw-mmv-download-attribution-cta-header' )
+ .text( mw.message( 'multimediaviewer-download-attribution-cta-header' ).text() );
+ this.$attributionCta = $( '<div>' )
+ .addClass( 'mw-mmv-download-attribution-cta' )
+ .append(
+ this.$attributionCtaHeader,
+ $( '<p>' )
+ .addClass( 'mw-mmv-download-attribution-cta-invite' )
+ .text( mw.message( 'multimediaviewer-download-attribution-cta' ).text() )
+ )
+ .appendTo( this.$attributionSection );
+ this.attributionInput = attributionInput;
+ this.$attributionCopy = this.$copyButton = $( '<button>' )
+ .addClass( 'mw-ui-button mw-mmv-button mw-mmv-dialog-copy' )
+ .click( function () {
+ // Select the text, and then try to copy the text.
+ // If the copy fails or is not supported, continue as if nothing had happened.
+ dl.attributionInput.select();
+ try {
+ if ( document.queryCommandSupported &&
+ document.queryCommandSupported( 'copy' ) ) {
+ document.execCommand( 'copy' );
+ }
+ } catch ( e ) {
+ // queryCommandSupported in Firefox pre-41 can throw errors when used with
+ // clipboard commands. We catch and ignore these and other copy-command-related
+ // errors here.
+ }
+ } )
+ .prop( 'title', mw.msg( 'multimediaviewer-download-attribution-copy' ) )
+ .text( mw.msg( 'multimediaviewer-download-attribution-copy' ) )
+ .tipsy( {
+ delayIn: mw.config.get( 'wgMultimediaViewer' ).tooltipDelay,
+ gravity: this.correctEW( 'se' )
+ } );
+
+ this.$attributionHowHeader = $( '<p>' )
+ .addClass( 'mw-mmv-download-attribution-how-header' )
+ .text( mw.message( 'multimediaviewer-download-attribution-cta-header' ).text() );
+ this.$attributionHow = $( '<div>' )
+ .addClass( 'mw-mmv-download-attribution-how' )
+ .append(
+ this.$attributionHowHeader,
+ this.attributionInput.$element,
+ this.$attributionCopy,
+ attributionSwitch.$element,
+ $( '<p>' )
+ .addClass( 'mw-mmv-download-attribution-close-button' )
+ .click( function ( e ) {
+ dl.$container.trigger( 'mmv-download-cta-close' );
+ dl.$attributionSection.addClass( 'mw-mmv-download-attribution-collapsed' );
+ e.stopPropagation();
+ } )
+ .text( ' ' )
+ )
+ .appendTo( this.$attributionSection );
+ };
+
+ /**
+ * Selects the specified attribution type.
+ *
+ * @param {string} [name='plain'] The attribution type to use ('plain' or 'html')
+ */
+ DP.selectAttribution = function ( name ) {
+ this.currentAttrView = name;
+
+ if ( this.currentAttrView === 'html' ) {
+ this.attributionInput.setValue( this.htmlCredit );
+ } else {
+ this.attributionInput.setValue( this.textCredit );
+ }
+ };
+
+ /**
+ * Registers listeners.
+ */
+ DP.attach = function () {
+ var download = this;
+
+ // Register handlers for switching between file sizes
+ this.downloadSizeMenu.getMenu().on( 'choose', function ( item ) {
+ download.handleSizeSwitch( item );
+ } );
+ this.$selectionArrow.on( 'click', function () {
+ download.downloadSizeMenu.getMenu().toggle();
+ } );
+
+ this.attributionInput.$element.find( 'input' )
+ .on( 'focus', this.selectAllOnEvent )
+ .on( 'mousedown click', this.onlyFocus );
+ };
+
+ /**
+ * Clears listeners.
+ */
+ DP.unattach = function () {
+ mw.mmv.ui.Element.prototype.unattach.call( this );
+
+ this.downloadSizeMenu.getMenu().off( 'choose' );
+ this.$selectionArrow.off( 'click' );
+
+ this.attributionInput.$element.find( 'input' )
+ .off( 'focus mousedown click' );
+ };
+
+ /**
+ * Handles size menu change events.
+ *
+ * @param {OO.ui.MenuOptionWidget} item
+ */
+ DP.handleSizeSwitch = function ( item ) {
+ var download = this,
+ value = item.getData();
+
+ if ( value.name === 'original' && this.image !== null ) {
+ this.setDownloadUrl( this.image.url );
+ this.setButtonText( value.name, this.getExtensionFromUrl( this.image.url ),
+ value.width, value.height );
+ } else {
+ // Disable download while we get the image
+ this.$downloadButton.addClass( 'disabledLink' );
+ // Set a temporary message. It will be updated once we have the file type.
+ this.setButtonText( value.name, this.imageExtension, value.width, value.height );
+
+ this.utils.getThumbnailUrlPromise( value.width ).done( function ( thumbnail ) {
+ download.setDownloadUrl( thumbnail.url );
+ download.setButtonText( value.name, download.getExtensionFromUrl( thumbnail.url ),
+ value.width, value.height );
+ } );
+ }
+ };
+
+ /**
+ * Sets the URL on the download button.
+ *
+ * @param {string} url
+ */
+ DP.setDownloadUrl = function ( url ) {
+ this.$downloadButton.attr( 'href', url + '?download' );
+ this.$previewLink.attr( 'href', url );
+
+ // Re-enable download
+ this.$downloadButton.removeClass( 'disabledLink' );
+ };
+
+ /**
+ * Sets the text of the download button.
+ *
+ * @param {string} sizeClass A size class such as 'small'
+ * @param {string} extension file extension
+ * @param {number} width
+ * @param {number} height
+ */
+ DP.setButtonText = function ( sizeClass, extension, width, height ) {
+ var sizeClasMessage, sizeMessage, dimensionMessage;
+
+ sizeClasMessage = mw.message( 'multimediaviewer-download-' + sizeClass + '-button-name' ).text();
+ dimensionMessage = mw.message( 'multimediaviewer-embed-dimensions', width, height ).text();
+ sizeMessage = mw.message( 'multimediaviewer-embed-dimensions-with-file-format',
+ dimensionMessage, extension ).text();
+
+ // Update button label and size strings to reflect new selected size
+ this.$downloadButton.html(
+ '<span class="mw-mmv-download-image-size-name">' + sizeClasMessage + '</span>' +
+ '<span class="mw-mmv-download-image-size">' + sizeMessage + '</span>'
+ );
+ };
+
+ /**
+ * Sets the text in the attribution input element.
+ *
+ * @param {mw.mmv.model.EmbedFileInfo} embed
+ */
+ DP.setAttributionText = function ( embed ) {
+ this.htmlCredit = this.formatter.getCreditHtml( embed );
+ this.textCredit = this.formatter.getCreditText( embed );
+ this.selectAttribution( this.currentAttrView );
+ };
+
+ /**
+ * Chops off the extension part of an URL.
+ *
+ * @param {string} url URL
+ * @return {string} Extension
+ */
+ DP.getExtensionFromUrl = function ( url ) {
+ var urlParts = url.split( '.' );
+ return urlParts[ urlParts.length - 1 ];
+ };
+
+ /**
+ * Sets the data on the element.
+ *
+ * @param {mw.mmv.model.Image} image
+ * @param {mw.mmv.model.Repo} repo
+ */
+ DP.set = function ( image, repo ) {
+ var attributionCtaMessage,
+ license = image && image.license,
+ sizeOptions = this.downloadSizeMenu.getMenu().getItems(),
+ sizes = this.utils.getPossibleImageSizesForHtml( image.width, image.height );
+
+ this.image = image;
+
+ this.utils.updateMenuOptions( sizes, sizeOptions );
+
+ this.downloadSizeMenu.$element.addClass( 'active' );
+
+ // Note: This extension will not be the real one for file types other than: png/gif/jpg/jpeg
+ this.imageExtension = image.title.getExtension().toLowerCase();
+
+ // Reset size menu to default item and update download button label now that we have the info
+ this.downloadSizeMenu.getMenu().chooseItem( this.defaultItem );
+
+ if ( image && repo ) {
+ this.setAttributionText( new mw.mmv.model.EmbedFileInfo( image, repo ) );
+ }
+
+ attributionCtaMessage = ( license && license.needsAttribution() ) ?
+ 'multimediaviewer-download-attribution-cta-header' :
+ 'multimediaviewer-download-optional-attribution-cta-header';
+ this.$attributionCtaHeader.text( mw.message( attributionCtaMessage ).text() );
+ this.$attributionHowHeader.text( mw.message( attributionCtaMessage ).text() );
+ };
+
+ /**
+ * @inheritdoc
+ */
+ DP.empty = function () {
+ this.downloadSizeMenu.getMenu().toggle( false );
+ this.downloadSizeMenu.$element.removeClass( 'active' );
+
+ this.$downloadButton.attr( 'href', '' );
+ this.$previewLink.attr( 'href', '' );
+ this.imageExtension = undefined;
+
+ this.image = null;
+ };
+
+ mw.mmv.ui.download.Pane = Pane;
+}( mediaWiki, jQuery, OO ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.download.pane.less b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.download.pane.less
new file mode 100644
index 00000000..85874556
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.download.pane.less
@@ -0,0 +1,206 @@
+@import 'mediawiki.ui/variables';
+@import '../mmv.mixins';
+
+@pane-padding: 10px;
+@attribution-color: #f8f9fa;
+@attribution-color-highlighted: #fff;
+@attribution-logo-size: 40px;
+@input-text-color: #222;
+
+.mw-mmv-download-pane {
+ padding: 0 @pane-padding;
+ position: relative;
+ height: 100%;
+
+ /* Disable link clicks */
+ a.disabledLink {
+ pointer-events: none;
+ cursor: default;
+ }
+
+ .mw-mmv-download-area {
+ padding-top: 20px;
+ padding-left: 10px;
+ padding-bottom: 60px;
+ width: 100%;
+
+ .mw-mmv-download-go-button {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ }
+
+ /* Selection size arrow element */
+ .mw-mmv-download-select-menu {
+ .unselectable();
+ background-image: /* @embed */ url( ../img/down.svg );
+ background-position: center center;
+ background-repeat: no-repeat;
+ min-width: 10px;
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+ border-left: 1px solid mix( #000, @colorProgressive, 20% );
+
+ &:hover {
+ border-bottom: 1px solid mix( #000, @colorProgressive, 20% );
+ }
+ }
+
+ .mw-mmv-download-image-size-name {
+ display: block;
+ }
+
+ .mw-mmv-download-image-size {
+ display: block;
+ font-size: small;
+ font-weight: normal;
+ }
+
+ .mw-mmv-download-preview-link {
+ display: inline-block;
+ margin-top: 12px;
+ margin-left: -6px;
+ font-size: 16px;
+ color: #54595d;
+
+ &:before {
+ display: inline-block;
+ vertical-align: middle;
+ content: ' ';
+ width: 44px;
+ height: 30px;
+ /* @embed */
+ background-image: url( img/open.svg );
+ background-size: contain;
+ background-position: right center;
+ background-repeat: no-repeat;
+ }
+ }
+
+ /* Pulldown size menu */
+ .mw-mmv-download-size {
+ text-align: start;
+ display: none;
+ margin-top: 0;
+ margin-right: 20px;
+ width: auto;
+
+ &.active {
+ display: block;
+ }
+
+ /* Changes to pulldown menu */
+ .oo-ui-dropdownWidget-handle {
+ display: none;
+ }
+
+ .oo-ui-menuOptionWidget {
+ display: block;
+ }
+
+ .oo-ui-widget-disabled {
+ display: none;
+ }
+
+ .oo-ui-labelElement-label {
+ margin-left: 20px;
+ }
+ }
+ }
+
+ .mw-mmv-download-attribution {
+ margin: 0 -@pane-padding;
+ padding: 0 @pane-padding @pane-padding;
+ background-color: @attribution-color;
+ color: #54595d;
+
+ &:hover {
+ background-color: @attribution-color-highlighted;
+ color: #222;
+ }
+
+ &-how {
+ position: relative;
+ display: block;
+ padding: 5px;
+ margin-bottom: 0;
+
+ .mw-mmv-download-attribution-close-button {
+ cursor: pointer;
+ position: absolute;
+ top: 5px;
+ right: 0;
+ width: 12px;
+ height: 12px;
+ /* @embed */
+ background-image: url( img/x_gray.svg );
+ }
+ }
+
+ &-how-header,
+ &-cta-header {
+ font-size: large;
+ font-weight: bold;
+ }
+
+ &-cta-header {
+ margin-bottom: 0;
+ }
+
+ &-cta-invite {
+ font-size: small;
+ margin: 0;
+ color: #72777d;
+ }
+
+ &-cta {
+ cursor: pointer;
+ display: none;
+ padding-left: @attribution-logo-size + 10px;
+
+ /* @embed */
+ background-image: url( img/user-ltr.svg );
+ background-repeat: no-repeat;
+ background-size: @attribution-logo-size;
+ background-position: left center;
+ }
+
+ &.mw-mmv-download-attribution-collapsed {
+ .mw-mmv-download-attribution-cta {
+ display: block;
+ }
+
+ .mw-mmv-download-attribution-how {
+ display: none;
+ }
+ }
+
+ .mw-mmv-download-attr-input {
+ // override OOUI fixed width
+ width: auto;
+
+ // margin between text widget and option switch widget
+ margin-bottom: 10px;
+
+ input[ readonly ] {
+ color: @input-text-color;
+ text-shadow: none;
+ }
+ }
+
+ .mw-mmv-dialog-copy {
+ // style rules based on .mw-mmv-share-page-link
+ float: right;
+ width: 1.5em;
+ height: 1.5em;
+
+ // position approximately to the middle - probably fragile but couldn't find a better way as
+ // the height of OOUI input widget has both em and px parts and not possible to calculate
+ // exactly
+ margin: 8px 0.5em 8px 0;
+ }
+ }
+}
+
+.mw-mmv-reuse-dialog.mw-mmv-reuse-download-active .mw-mmv-reuse-down-arrow {
+ background-color: @attribution-color;
+}
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.js
new file mode 100644
index 00000000..e498e803
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.js
@@ -0,0 +1,268 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $, oo ) {
+ var EP,
+ cachedRTL;
+
+ /**
+ * Represents a UI element.
+ *
+ * @class mw.mmv.ui.Element
+ * @abstract
+ * @constructor
+ * @param {jQuery} $container
+ */
+ function Element( $container ) {
+ oo.EventEmitter.call( this );
+
+ /** @property {jQuery} $container The element that contains the UI element. */
+ this.$container = $container;
+
+ /** @property {Object.<string, string[]>} eventsRegistered Events that this element has registered with the DOM. */
+ this.eventsRegistered = {};
+
+ /**
+ * @property {Object.<string, jQuery>} $inlineStyles a list of `<style>` elements in the head
+ * which we use to manipulate pseudo-classes and pseudo-elements.
+ */
+ this.$inlineStyles = [];
+
+ /**
+ * Stores named timeouts. See setTimer().
+ *
+ * @private
+ * @property {Object.<string, {timeout: Object, handler: function(), delay: number}>}
+ */
+ this.timers = {};
+ }
+
+ oo.mixinClass( Element, oo.EventEmitter );
+
+ EP = Element.prototype;
+
+ /**
+ * Checks whether the document is RTL. Assumes it doesn't change.
+ *
+ * @return {boolean}
+ */
+ EP.isRTL = function () {
+ if ( cachedRTL === undefined ) {
+ cachedRTL = $( document.body ).hasClass( 'rtl' );
+ }
+
+ return cachedRTL;
+ };
+
+ /**
+ * Sets the data for the element.
+ *
+ * @abstract
+ */
+ EP.set = function () {};
+
+ /**
+ * Empties the element.
+ *
+ * @abstract
+ */
+ EP.empty = function () {};
+
+ /**
+ * Registers listeners.
+ *
+ * @abstract
+ */
+ EP.attach = function () {};
+
+ /**
+ * Clears listeners.
+ *
+ * @abstract
+ */
+ EP.unattach = function () {
+ this.clearEvents();
+ };
+
+ /**
+ * Add event handler in a way that will be auto-cleared on lightbox close
+ *
+ * TODO: Unit tests
+ *
+ * @param {string} name Name of event, like 'keydown'
+ * @param {Function} handler Callback for the event
+ */
+ EP.handleEvent = function ( name, handler ) {
+ if ( this.eventsRegistered[ name ] === undefined ) {
+ this.eventsRegistered[ name ] = [];
+ }
+ this.eventsRegistered[ name ].push( handler );
+ $( document ).on( name, handler );
+ };
+
+ /**
+ * Remove all events that have been registered on this element.
+ *
+ * TODO: Unit tests
+ */
+ EP.clearEvents = function () {
+ var i, handlers, thisevent,
+ events = Object.keys( this.eventsRegistered );
+
+ for ( i = 0; i < events.length; i++ ) {
+ thisevent = events[ i ];
+ handlers = this.eventsRegistered[ thisevent ];
+ while ( handlers.length > 0 ) {
+ $( document ).off( thisevent, handlers.pop() );
+ }
+ }
+ };
+
+ /**
+ * Manipulate CSS directly. This is needed to set styles for pseudo-classes and pseudo-elements.
+ *
+ * @param {string} key some name to identify the style
+ * @param {string|null} style a CSS snippet (set to null to delete the given style)
+ */
+ EP.setInlineStyle = function ( key, style ) {
+
+ if ( !this.$inlineStyles ) {
+ this.$inlineStyles = [];
+ }
+
+ if ( !this.$inlineStyles[ key ] ) {
+ if ( !style ) {
+ return;
+ }
+
+ this.$inlineStyles[ key ] = $( '<style type="text/css" />' ).appendTo( 'head' );
+ }
+
+ this.$inlineStyles[ key ].html( style || '' );
+ };
+
+ /**
+ * Sets a timer. This is a shortcut to using the native setTimout and then storing
+ * the reference, with some small differences for convenience:
+ * - setting the same timer again clears the old one
+ * - callbacks have the element as their context
+ * Timers are local to the element.
+ * See also clearTimer() and resetTimer().
+ *
+ * @param {string} name
+ * @param {function()} callback
+ * @param {number} delay delay in milliseconds
+ */
+ EP.setTimer = function ( name, callback, delay ) {
+ var element = this;
+
+ this.clearTimer( name );
+ this.timers[ name ] = {
+ timeout: null,
+ handler: callback,
+ delay: delay
+ };
+ this.timers[ name ].timeout = setTimeout( function () {
+ delete element.timers[ name ];
+ callback.call( element );
+ }, delay );
+ };
+
+ /**
+ * Clears a timer. See setTimer().
+ *
+ * @param {string} name
+ */
+ EP.clearTimer = function ( name ) {
+ if ( name in this.timers ) {
+ clearTimeout( this.timers[ name ].timeout );
+ delete this.timers[ name ];
+ }
+ };
+
+ /**
+ * Resets a timer, so that its delay will be relative to when resetTimer() was called, not when
+ * the timer was created. Optionally changes the delay as well.
+ * Resetting a timer that does not exist or has already fired has no effect.
+ * See setTimer().
+ *
+ * @param {string} name
+ * @param {number} [delay] delay in milliseconds
+ */
+ EP.resetTimer = function ( name, delay ) {
+ if ( name in this.timers ) {
+ if ( delay === undefined ) {
+ delay = this.timers[ name ].delay;
+ }
+ this.setTimer( name, this.timers[ name ].handler, delay );
+ }
+ };
+
+ /**
+ * Makes the entire input/textarea selected when focused.
+ * Invoked with that input/textarea as context.
+ */
+ EP.selectAllOnEvent = function () {
+ try {
+ this.select();
+ } catch ( e ) {
+ // IE doesn't like when select() is called during the onfocus handler
+ }
+ };
+
+ /**
+ * Reduces the action of clicks to solely focusing the input/textarea.
+ * Essentialy disables clicking inside the text to select a portion of it.
+ * Invoked with that input/textarea as context.
+ *
+ * @param {jQuery.Event} e
+ * @return {boolean} False to prevent default event
+ */
+ EP.onlyFocus = function ( e ) {
+ this.focus();
+ e.preventDefault();
+ return false;
+ };
+
+ /**
+ * Flips E (east) and W (west) directions in RTL documents.
+ *
+ * @param {string} keyword a keyword where the first 'e' or 'w' character means a direction (such as a
+ * tipsy gravity parameter)
+ * @return {string}
+ */
+ EP.correctEW = function ( keyword ) {
+ if ( this.isRTL() ) {
+ keyword = keyword.replace( /[ew]/i, function ( dir ) {
+ if ( dir === 'e' ) {
+ return 'w';
+ } else if ( dir === 'E' ) {
+ return 'W';
+ } else if ( dir === 'w' ) {
+ return 'e';
+ } else if ( dir === 'W' ) {
+ return 'E';
+ }
+ } );
+ }
+ return keyword;
+ };
+
+ mw.mmv.ui = {};
+ mw.mmv.ui.reuse = {};
+ mw.mmv.ui.Element = Element;
+}( mediaWiki, jQuery, OO ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.metadataPanel.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.metadataPanel.js
new file mode 100644
index 00000000..182173fa
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.metadataPanel.js
@@ -0,0 +1,886 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $, oo ) {
+ // Shortcut for prototype later
+ var MPP;
+
+ /**
+ * Represents the metadata panel in the viewer
+ *
+ * @class mw.mmv.ui.MetadataPanel
+ * @extends mw.mmv.ui.Element
+ * @constructor
+ * @param {jQuery} $container The container for the panel (.mw-mmv-post-image).
+ * @param {jQuery} $aboveFold The brighter headline of the metadata panel (.mw-mmv-above-fold).
+ * Called "aboveFold" for historical reasons, but actually a part of the next sibling of the element
+ * is also above the fold (bottom of the screen).
+ * @param {mw.storage} localStorage the localStorage object, for dependency injection
+ * @param {mw.mmv.Config} config A configuration object.
+ */
+ function MetadataPanel( $container, $aboveFold, localStorage, config ) {
+ mw.mmv.ui.Element.call( this, $container );
+
+ this.$aboveFold = $aboveFold;
+
+ /** @property {mw.mmv.Config} config - */
+ this.config = config;
+
+ /** @property {mw.mmv.HtmlUtils} htmlUtils - */
+ this.htmlUtils = new mw.mmv.HtmlUtils();
+
+ this.initializeHeader( localStorage );
+ this.initializeImageMetadata();
+ this.initializeAboutLinks();
+ }
+ oo.inheritClass( MetadataPanel, mw.mmv.ui.Element );
+ MPP = MetadataPanel.prototype;
+
+ /**
+ * Maximum number of restriction icons before default icon is used
+ *
+ * @property MAX_RESTRICT
+ * @static
+ */
+ MetadataPanel.MAX_RESTRICT = 4;
+
+ /**
+ * FIXME this should be in the jquery.fullscreen plugin.
+ *
+ * @return {boolean}
+ */
+ MPP.isFullscreened = function () {
+ return $( this.$container ).closest( '.jq-fullscreened' ).length > 0;
+ };
+
+ MPP.attach = function () {
+ var panel = this;
+
+ this.scroller.attach();
+ this.buttons.attach();
+ this.title.attach();
+ this.creditField.attach();
+
+ this.$title
+ .add( this.$authorAndSource )
+ .add( this.title.$ellipsis )
+ .add( this.creditField.$ellipsis )
+ .each( function () {
+ $( this ).tipsy( 'enable' );
+ } )
+ .on( 'click.mmv-mp', function ( e ) {
+ var clickTargetIsLink = $( e.target ).is( 'a' ),
+ clickTargetIsTruncated = !!$( e.target ).closest( '.mw-mmv-ttf-truncated' ).length,
+ someTextIsExpanded = !!$( e.target ).closest( '.mw-mmv-untruncated' ).length;
+
+ if (
+ !clickTargetIsLink && // don't interfere with clicks on links in the text
+ clickTargetIsTruncated && // don't expand when non-truncated text is clicked
+ !someTextIsExpanded // ignore clicks if text is already expanded
+ ) {
+ if ( panel.isFullscreened() ) {
+ panel.revealTruncatedText();
+ } else {
+ panel.scroller.toggle( 'up' );
+ }
+ }
+ } );
+
+ $( this.$container ).on( 'mmv-metadata-open.mmv-mp mmv-metadata-reveal-truncated-text.mmv-mp', function () {
+ panel.revealTruncatedText();
+ } ).on( 'mmv-metadata-close.mmv-mp', function () {
+ panel.hideTruncatedText();
+ } ).on( 'mouseleave.mmv-mp', function () {
+ var duration;
+
+ if ( panel.isFullscreened() ) {
+ duration = parseFloat( panel.$container.css( 'transition-duration' ) ) * 1000 || 0;
+ panel.panelShrinkTimeout = setTimeout( function () {
+ panel.hideTruncatedText();
+ }, duration );
+ }
+ } ).on( 'mouseenter.mmv-mp', function () {
+ clearTimeout( panel.panelShrinkTimeout );
+ } ).on( 'mmv-permission-grow.mmv-mp', function () {
+ panel.$permissionLink
+ .text( mw.message( 'multimediaviewer-permission-link-hide' ).text() );
+ } ).on( 'mmv-permission-shrink.mmv-mp', function () {
+ panel.$permissionLink
+ .text( mw.message( 'multimediaviewer-permission-link' ).text() );
+ } );
+
+ this.handleEvent( 'jq-fullscreen-change.lip', function () {
+ panel.hideTruncatedText();
+ } );
+ };
+
+ MPP.unattach = function () {
+ this.scroller.freezeHeight();
+
+ this.$title
+ .add( this.title.$ellipsis )
+ .add( this.$authorAndSource )
+ .add( this.creditField.$ellipsis )
+ .each( function () {
+ $( this ).tipsy( 'hide' ).tipsy( 'disable' );
+ } )
+ .off( 'click.mmv-mp' );
+
+ $( this.$container ).off( '.mmv-mp' );
+
+ this.scroller.unattach();
+ this.buttons.unattach();
+ this.clearEvents();
+ };
+
+ MPP.empty = function () {
+ this.scroller.freezeHeight();
+ this.scroller.empty();
+
+ this.buttons.empty();
+
+ this.description.empty();
+ this.permission.empty();
+
+ this.$title.removeClass( 'error' );
+ this.title.empty();
+ this.creditField.empty();
+
+ this.$license.empty().prop( 'href', '#' );
+ this.$licenseLi.addClass( 'empty' );
+ this.$permissionLink.hide();
+ this.$restrictions.children().hide();
+
+ this.$filename.empty();
+ this.$filenamePrefix.empty();
+ this.$filenameLi.addClass( 'empty' );
+
+ this.$datetime.empty();
+ this.$datetimeLi.addClass( 'empty' );
+
+ this.$location.empty();
+ this.$locationLi.addClass( 'empty' );
+
+ this.progressBar.empty();
+
+ this.$container.removeClass( 'mw-mmv-untruncated' );
+ };
+
+ /* Initialization methods */
+
+ /**
+ * Initializes the header, which contains the title, credit, and license elements.
+ *
+ * @param {mw.storage} localStorage the localStorage object, for dependency injection
+ */
+ MPP.initializeHeader = function ( localStorage ) {
+ this.progressBar = new mw.mmv.ui.ProgressBar( this.$aboveFold );
+
+ this.scroller = new mw.mmv.ui.MetadataPanelScroller( this.$container, this.$aboveFold,
+ localStorage );
+
+ this.$titleDiv = $( '<div>' )
+ .addClass( 'mw-mmv-title-contain' )
+ .appendTo( this.$aboveFold );
+
+ this.$container.append( this.$aboveFold );
+
+ this.initializeButtons(); // float, needs to be on top
+ this.initializeTitle();
+ };
+
+ /**
+ * Initializes the title elements.
+ */
+ MPP.initializeTitle = function () {
+ this.$titlePara = $( '<p>' )
+ .addClass( 'mw-mmv-title-para' )
+ .appendTo( this.$aboveFold );
+
+ this.$title = $( '<span>' )
+ .addClass( 'mw-mmv-title' );
+
+ this.title = new mw.mmv.ui.TruncatableTextField( this.$titlePara, this.$title, {
+ styles: [ 'mw-mmv-title-small', 'mw-mmv-title-smaller' ]
+ } );
+ this.title.setTitle(
+ mw.message( 'multimediaviewer-title-popup-text' ),
+ mw.message( 'multimediaviewer-title-popup-text-more' )
+ );
+
+ this.$title.add( this.title.$ellipsis ).tipsy( {
+ delayIn: mw.config.get( 'wgMultimediaViewer' ).tooltipDelay,
+ gravity: this.correctEW( 'sw' )
+ } );
+ };
+
+ MPP.initializeButtons = function () {
+ this.buttons = new mw.mmv.ui.StripeButtons( this.$titleDiv );
+ };
+
+ /**
+ * Initializes the main body of metadata elements.
+ */
+ MPP.initializeImageMetadata = function () {
+ this.$container.addClass( 'mw-mmv-ttf-ellipsis-container' );
+
+ this.$imageMetadata = $( '<div>' )
+ .addClass( 'mw-mmv-image-metadata' )
+ .appendTo( this.$container );
+
+ this.$imageMetadataLeft = $( '<div>' )
+ .addClass( 'mw-mmv-image-metadata-column mw-mmv-image-metadata-desc-column' )
+ .appendTo( this.$imageMetadata );
+
+ this.$imageMetadataRight = $( '<div>' )
+ .addClass( 'mw-mmv-image-metadata-column mw-mmv-image-metadata-links-column' )
+ .appendTo( this.$imageMetadata );
+
+ this.initializeCredit();
+ this.description = new mw.mmv.ui.Description( this.$imageMetadataLeft );
+ this.permission = new mw.mmv.ui.Permission( this.$imageMetadataLeft, this.scroller );
+ this.initializeImageLinks();
+ };
+
+ /**
+ * Initializes the credit elements.
+ */
+ MPP.initializeCredit = function () {
+ this.$credit = $( '<p>' )
+ .addClass( 'mw-mmv-credit empty' )
+ .appendTo( this.$imageMetadataLeft )
+ .on( 'click.mmv-mp', '.mw-mmv-credit-fallback', function () {
+ mw.mmv.actionLogger.log( 'author-page' );
+ } );
+
+ // we need an inline container for tipsy, otherwise it would be centered weirdly
+ this.$authorAndSource = $( '<span>' )
+ .addClass( 'mw-mmv-source-author' )
+ .on( 'click', '.mw-mmv-author a', function () {
+ mw.mmv.actionLogger.log( 'author-page' );
+ } )
+ .on( 'click', '.mw-mmv-source a', function () {
+ mw.mmv.actionLogger.log( 'source-page' );
+ } );
+
+ this.creditField = new mw.mmv.ui.TruncatableTextField(
+ this.$credit,
+ this.$authorAndSource,
+ { styles: [] }
+ );
+
+ this.creditField.setTitle(
+ mw.message( 'multimediaviewer-credit-popup-text' ),
+ mw.message( 'multimediaviewer-credit-popup-text-more' )
+ );
+
+ this.$authorAndSource.add( this.creditField.$ellipsis ).tipsy( {
+ delayIn: mw.config.get( 'wgMultimediaViewer' ).tooltipDelay,
+ gravity: this.correctEW( 'sw' )
+ } );
+ };
+
+ /**
+ * Initializes the list of image metadata on the right side of the panel.
+ */
+ MPP.initializeImageLinks = function () {
+ this.$imageLinkDiv = $( '<div>' )
+ .addClass( 'mw-mmv-image-links-div' )
+ .appendTo( this.$imageMetadataRight );
+
+ this.$imageLinks = $( '<ul>' )
+ .addClass( 'mw-mmv-image-links' )
+ .appendTo( this.$imageLinkDiv );
+
+ this.initializeLicense();
+ this.initializeFilename();
+ this.initializeDatetime();
+ this.initializeLocation();
+ };
+
+ /**
+ * Initializes the license elements.
+ */
+ MPP.initializeLicense = function () {
+ var panel = this;
+
+ this.$licenseLi = $( '<li>' )
+ .addClass( 'mw-mmv-license-li empty' )
+ .appendTo( this.$imageLinks );
+
+ this.$license = $( '<a>' )
+ .addClass( 'mw-mmv-license' )
+ .prop( 'href', '#' )
+ .appendTo( this.$licenseLi )
+ .on( 'click', function () {
+ mw.mmv.actionLogger.log( 'license-page' );
+ } );
+
+ this.$restrictions = $( '<span>' )
+ .addClass( 'mw-mmv-restrictions' )
+ .appendTo( this.$licenseLi );
+
+ this.$permissionLink = $( '<span>' )
+ .addClass( 'mw-mmv-permission-link mw-mmv-label' )
+ .text( mw.message( 'multimediaviewer-permission-link' ).text() )
+ .appendTo( this.$licenseLi )
+ .hide()
+ .on( 'click', function () {
+ if ( panel.permission.isFullSize() ) {
+ panel.permission.shrink();
+ } else {
+ panel.permission.grow();
+ panel.scroller.toggle( 'up' );
+ }
+ return false;
+ } );
+ };
+
+ /**
+ * Initializes the filename element.
+ */
+ MPP.initializeFilename = function () {
+ this.$filenameLi = $( '<li>' )
+ .addClass( 'mw-mmv-filename-li empty' )
+ .appendTo( this.$imageLinks );
+
+ this.$filenamePrefix = $( '<span>' )
+ .addClass( 'mw-mmv-filename-prefix' )
+ .appendTo( this.$filenameLi );
+
+ this.$filename = $( '<span>' )
+ .addClass( 'mw-mmv-filename' )
+ .appendTo( this.$filenameLi );
+ };
+
+ /**
+ * Initializes the upload date/time element.
+ */
+ MPP.initializeDatetime = function () {
+ this.$datetimeLi = $( '<li>' )
+ .addClass( 'mw-mmv-datetime-li empty' )
+ .appendTo( this.$imageLinks );
+
+ this.$datetime = $( '<span>' )
+ .addClass( 'mw-mmv-datetime' )
+ .appendTo( this.$datetimeLi );
+ };
+
+ /**
+ * Initializes the geolocation element.
+ */
+ MPP.initializeLocation = function () {
+ this.$locationLi = $( '<li>' )
+ .addClass( 'mw-mmv-location-li empty' )
+ .appendTo( this.$imageLinks );
+
+ this.$location = $( '<a>' )
+ .addClass( 'mw-mmv-location' )
+ .appendTo( this.$locationLi )
+ .click( function () { mw.mmv.actionLogger.log( 'location-page' ); } );
+ };
+
+ /**
+ * Initializes two about links at the bottom of the panel.
+ */
+ MPP.initializeAboutLinks = function () {
+ var separator = ' | ';
+
+ this.$mmvAboutLink = $( '<a>' )
+ .prop( 'href', mw.config.get( 'wgMultimediaViewer' ).infoLink )
+ .text( mw.message( 'multimediaviewer-about-mmv' ).text() )
+ .addClass( 'mw-mmv-about-link' )
+ .click( function () { mw.mmv.actionLogger.log( 'about-page' ); } );
+
+ this.$mmvDiscussLink = $( '<a>' )
+ .prop( 'href', mw.config.get( 'wgMultimediaViewer' ).discussionLink )
+ .text( mw.message( 'multimediaviewer-discuss-mmv' ).text() )
+ .addClass( 'mw-mmv-discuss-link' )
+ .click( function () { mw.mmv.actionLogger.log( 'discuss-page' ); } );
+
+ this.$mmvHelpLink = $( '<a>' )
+ .prop( 'href', mw.config.get( 'wgMultimediaViewer' ).helpLink )
+ .text( mw.message( 'multimediaviewer-help-mmv' ).text() )
+ .addClass( 'mw-mmv-help-link' )
+ .click( function () { mw.mmv.actionLogger.log( 'help-page' ); } );
+
+ this.$mmvAboutLinks = $( '<div>' )
+ .addClass( 'mw-mmv-about-links' )
+ .append(
+ this.$mmvAboutLink,
+ separator,
+ this.$mmvDiscussLink,
+ separator,
+ this.$mmvHelpLink
+ )
+ .appendTo( this.$imageMetadata );
+ };
+
+ /* Setters */
+
+ /**
+ * Sets the image title at the top of the metadata panel.
+ * The title will be the first one available form the options below:
+ * - the image caption
+ * - the description from the filepage
+ * - the filename (without extension)
+ *
+ * @param {mw.mmv.LightboxImage} image
+ * @param {mw.mmv.model.Image} imageData
+ */
+ MPP.setTitle = function ( image, imageData ) {
+ var title;
+
+ if ( image.caption ) {
+ title = image.caption;
+ } else if ( imageData.description ) {
+ title = imageData.description;
+ } else {
+ title = image.filePageTitle.getNameText();
+ }
+
+ this.title.set( title );
+ };
+
+ /**
+ * Sets the upload or creation date and time in the panel
+ *f
+ * @param {string} date The formatted date to set.
+ * @param {boolean} created Whether this is the creation date
+ */
+ MPP.setDateTime = function ( date, created ) {
+ this.$datetime.text(
+ mw.message(
+ 'multimediaviewer-datetime-' + ( created ? 'created' : 'uploaded' ),
+ date
+ ).text()
+ );
+
+ this.$datetimeLi.removeClass( 'empty' );
+ };
+
+ /**
+ * Sets the file name in the panel.
+ *
+ * @param {string} filename The file name to set, without prefix
+ */
+ MPP.setFileName = function ( filename ) {
+ this.$filenamePrefix.text( 'File:' );
+ this.$filename.text( filename );
+
+ this.$filenameLi.removeClass( 'empty' );
+ };
+
+ /**
+ * Set source and author.
+ *
+ * @param {string} attribution Custom attribution string
+ * @param {string} source With unsafe HTML
+ * @param {string} author With unsafe HTML
+ * @param {number} authorCount
+ * @param {string} filepageUrl URL of the file page (used when other data is not available)
+ */
+ MPP.setCredit = function ( attribution, source, author, authorCount, filepageUrl ) {
+ // sanitization will be done by TruncatableTextField.set()
+ if ( attribution && ( authorCount <= 1 || !authorCount ) ) {
+ this.creditField.set( this.wrapAttribution( attribution ) );
+ } else if ( author && source ) {
+ this.creditField.set(
+ mw.message(
+ 'multimediaviewer-credit',
+ this.wrapAuthor( author, authorCount, filepageUrl ),
+ this.wrapSource( source )
+ ).plain()
+ );
+ } else if ( author ) {
+ this.creditField.set( this.wrapAuthor( author, authorCount, filepageUrl ) );
+ } else if ( source ) {
+ this.creditField.set( this.wrapSource( source ) );
+ } else {
+ this.creditField.set(
+ $( '<a>' )
+ .addClass( 'mw-mmv-credit-fallback' )
+ .prop( 'href', filepageUrl )
+ .text( mw.message( 'multimediaviewer-credit-fallback' ).plain() )
+ );
+ }
+
+ this.$credit.removeClass( 'empty' );
+ };
+
+ /**
+ * Wraps a source string it with MediaViewer styles
+ *
+ * @param {string} source Warning - unsafe HTML sometimes goes here
+ * @return {string} unsafe HTML
+ */
+ MPP.wrapSource = function ( source ) {
+ return $( '<span>' )
+ .addClass( 'mw-mmv-source' )
+ .append( $.parseHTML( source ) )
+ .get( 0 ).outerHTML;
+ };
+
+ /**
+ * Wraps an author string with MediaViewer styles
+ *
+ * @param {string} author Warning - unsafe HTML sometimes goes here
+ * @param {number} authorCount
+ * @param {string} filepageUrl URL of the file page (used when some author data is not available)
+ * @return {string} unsafe HTML
+ */
+ MPP.wrapAuthor = function ( author, authorCount, filepageUrl ) {
+ var moreText,
+ $wrapper = $( '<span>' );
+
+ $wrapper.addClass( 'mw-mmv-author' );
+
+ if ( authorCount > 1 ) {
+ moreText = this.htmlUtils.jqueryToHtml(
+ $( '<a>' )
+ .addClass( 'mw-mmv-more-authors' )
+ .text( mw.message( 'multimediaviewer-multiple-authors', authorCount - 1 ).text() )
+ .attr( 'href', filepageUrl )
+ );
+ $wrapper.append( mw.message( 'multimediaviewer-multiple-authors-combine', author, moreText ).text() );
+ } else {
+ $wrapper.append( author );
+ }
+
+ return $wrapper.get( 0 ).outerHTML;
+ };
+
+ /**
+ * Wraps an attribution string with MediaViewer styles
+ *
+ * @param {string} attribution Warning - unsafe HTML sometimes goes here
+ * @return {string} unsafe HTML
+ */
+ MPP.wrapAttribution = function ( attribution ) {
+ return $( '<span>' )
+ .addClass( 'mw-mmv-author' )
+ .addClass( 'mw-mmv-source' )
+ .append( $.parseHTML( attribution ) )
+ .get( 0 ).outerHTML;
+ };
+
+ /**
+ * Sets the license display in the panel
+ *
+ * @param {mw.mmv.model.License|null} license license data (could be missing)
+ * @param {string} filePageUrl URL of the file description page
+ */
+ MPP.setLicense = function ( license, filePageUrl ) {
+ var shortName, url, isCc, isPd;
+
+ if ( license ) {
+ shortName = license.getShortName();
+ url = license.deedUrl || filePageUrl;
+ isCc = license.isCc();
+ isPd = license.isPd();
+ } else {
+ shortName = mw.message( 'multimediaviewer-license-default' ).text();
+ url = filePageUrl;
+ isCc = isPd = false;
+ }
+
+ this.$license
+ .text( shortName )
+ .prop( 'href', url )
+ .prop( 'target', license && license.deedUrl ? '_blank' : '' );
+
+ this.$licenseLi
+ .toggleClass( 'cc-license', isCc )
+ .toggleClass( 'pd-license', isPd )
+ .removeClass( 'empty' );
+ };
+
+ /**
+ * Set an extra permission text which should be displayed.
+ *
+ * @param {string} permission
+ */
+ MPP.setPermission = function ( permission ) {
+ this.$permissionLink.show();
+ this.permission.set( permission );
+ };
+
+ /**
+ * Sets any special restrictions that should be displayed.
+ *
+ * @param {string[]} restrictions Array of restrictions
+ */
+ MPP.setRestrictions = function ( restrictions ) {
+ var panel = this,
+ restrictionsSet = {},
+ showDefault = false,
+ validRestrictions = 0;
+
+ $.each( restrictions, function ( index, value ) {
+ if ( !mw.message( 'multimediaviewer-restriction-' + value ).exists() || value === 'default' || index + 1 > MetadataPanel.MAX_RESTRICT ) {
+ showDefault = true; // If the restriction isn't defined or there are more than MAX_RESTRICT of them, show a generic symbol at the end
+ return;
+ }
+ if ( restrictionsSet[ value ] ) {
+ return; // Only show one of each symbol
+ } else {
+ restrictionsSet[ value ] = true;
+ }
+
+ panel.$restrictions.append( panel.createRestriction( value ) );
+ validRestrictions++; // See how many defined restrictions are added so we know which default i18n msg to use
+ } );
+
+ if ( showDefault ) {
+ if ( validRestrictions ) {
+ panel.$restrictions.append( panel.createRestriction( 'default-and-others' ) );
+ } else {
+ panel.$restrictions.append( panel.createRestriction( 'default' ) );
+ }
+ }
+ };
+
+ /**
+ * Helper function that generates restriction labels
+ *
+ * @param {string} type Restriction type
+ * @return {jQuery} jQuery object of label
+ */
+ MPP.createRestriction = function ( type ) {
+ var $label = $( '<span>' )
+ .addClass( 'mw-mmv-label mw-mmv-restriction-label' )
+ .prop( 'title', mw.message( 'multimediaviewer-restriction-' + type ).text() )
+ .tipsy( {
+ delay: mw.config.get( 'wgMultimediaViewer' ).tooltipDelay,
+ gravity: this.correctEW( 'se' )
+ } );
+
+ $( '<span>' )
+ .addClass( 'mw-mmv-restriction-label-inner mw-mmv-restriction-' +
+ ( type === 'default-and-others' ? 'default' : type ) )
+ .text( mw.message( 'multimediaviewer-restriction-' + type ).text() )
+ .appendTo( $label );
+
+ return $label;
+ };
+
+ /**
+ * Sets location data in the interface.
+ *
+ * @param {mw.mmv.model.Image} imageData
+ */
+ MPP.setLocationData = function ( imageData ) {
+ var latsec, latitude, latmsg, latdeg, latremain, latmin,
+ longsec, longitude, longmsg, longdeg, longremain, longmin,
+ language;
+
+ if ( !imageData.hasCoords() ) {
+ return;
+ }
+
+ latitude = imageData.latitude >= 0 ? imageData.latitude : imageData.latitude * -1;
+ latmsg = 'multimediaviewer-geoloc-' + ( imageData.latitude >= 0 ? 'north' : 'south' );
+ latdeg = Math.floor( latitude );
+ latremain = latitude - latdeg;
+ latmin = Math.floor( ( latremain ) * 60 );
+
+ longitude = imageData.longitude >= 0 ? imageData.longitude : imageData.longitude * -1;
+ longmsg = 'multimediaviewer-geoloc-' + ( imageData.longitude >= 0 ? 'east' : 'west' );
+ longdeg = Math.floor( longitude );
+ longremain = longitude - longdeg;
+ longmin = Math.floor( ( longremain ) * 60 );
+
+ longremain -= longmin / 60;
+ latremain -= latmin / 60;
+ latsec = Math.round( latremain * 100 * 60 * 60 ) / 100;
+ longsec = Math.round( longremain * 100 * 60 * 60 ) / 100;
+
+ this.$location.text(
+ mw.message( 'multimediaviewer-geolocation',
+ mw.message(
+ 'multimediaviewer-geoloc-coords',
+
+ mw.message(
+ 'multimediaviewer-geoloc-coord',
+ mw.language.convertNumber( latdeg ),
+ mw.language.convertNumber( latmin ),
+ mw.language.convertNumber( latsec ),
+ mw.message( latmsg ).text()
+ ).text(),
+
+ mw.message(
+ 'multimediaviewer-geoloc-coord',
+ mw.language.convertNumber( longdeg ),
+ mw.language.convertNumber( longmin ),
+ mw.language.convertNumber( longsec ),
+ mw.message( longmsg ).text()
+ ).text()
+ ).text()
+ ).text()
+ );
+
+ $.each( mw.language.data, function ( key ) {
+ language = key;
+ return false;
+ } );
+
+ this.$location.prop( 'href', (
+ '//tools.wmflabs.org/geohack/geohack.php?pagename=' +
+ 'File:' + imageData.title.getMain() +
+ '&params=' +
+ Math.abs( imageData.latitude ) + ( imageData.latitude >= 0 ? '_N_' : '_S_' ) +
+ Math.abs( imageData.longitude ) + ( imageData.longitude >= 0 ? '_E_' : '_W_' ) +
+ '&language=' + language
+ ) );
+
+ this.$locationLi.removeClass( 'empty' );
+ };
+
+ /**
+ * Set all the image information in the panel
+ *
+ * @param {mw.mmv.LightboxImage} image
+ * @param {mw.mmv.model.Image} imageData
+ * @param {mw.mmv.model.Repo} repoData
+ */
+ MPP.setImageInfo = function ( image, imageData, repoData ) {
+ var panel = this;
+
+ mw.mmv.attributionLogger.logAttribution( imageData );
+
+ if ( imageData.creationDateTime ) {
+ // Use the raw date until moment can try to interpret it
+ panel.setDateTime( imageData.creationDateTime );
+
+ this.formatDate( imageData.creationDateTime ).then( function ( formattedDate ) {
+ panel.setDateTime( formattedDate, true );
+ } );
+ } else if ( imageData.uploadDateTime ) {
+ // Use the raw date until moment can try to interpret it
+ panel.setDateTime( imageData.uploadDateTime );
+
+ this.formatDate( imageData.uploadDateTime ).then( function ( formattedDate ) {
+ panel.setDateTime( formattedDate );
+ } );
+ }
+
+ this.buttons.set( imageData, repoData );
+ this.description.set( imageData.description, image.caption );
+
+ this.setLicense( imageData.license, imageData.descriptionUrl );
+
+ this.setFileName( imageData.title.getMainText() );
+
+ // these handle text truncation and should be called when everything that can push text down
+ // (e.g. floated buttons) has already been laid out
+ this.setTitle( image, imageData );
+ this.setCredit( imageData.attribution, imageData.source, imageData.author, imageData.authorCount, imageData.descriptionUrl );
+
+ if ( imageData.permission ) {
+ this.setPermission( imageData.permission );
+ }
+
+ if ( imageData.restrictions ) {
+ this.setRestrictions( imageData.restrictions );
+ }
+
+ this.setLocationData( imageData );
+
+ this.resetTruncatedText();
+ this.scroller.unfreezeHeight();
+ };
+
+ /**
+ * Show an error message, in case the data could not be loaded
+ *
+ * @param {string} title image title
+ * @param {string} error error message
+ */
+ MPP.showError = function ( title, error ) {
+ this.$credit.text( mw.message( 'multimediaviewer-metadata-error', error ).text() );
+ this.$title.html( title );
+ };
+
+ /**
+ * Transforms a date string into localized, human-readable format.
+ * Unrecognized strings are returned unchanged.
+ *
+ * @param {string} dateString
+ * @return {jQuery.Deferred}
+ */
+ MPP.formatDate = function ( dateString ) {
+ var deferred = $.Deferred(),
+ date;
+
+ mw.loader.using( 'moment', function () {
+ /* global moment */
+ date = moment( dateString );
+
+ if ( date.isValid() ) {
+ deferred.resolve( date.format( 'LL' ) );
+ } else {
+ deferred.resolve( dateString );
+ }
+ }, function ( error ) {
+ deferred.reject( error );
+ if ( window.console && window.console.error ) {
+ window.console.error( 'mw.loader.using error when trying to load moment', error );
+ }
+ } );
+
+ return deferred.promise();
+ };
+
+ /**
+ * Shows truncated text in the title and credit (this also rearranges the layout a bit).
+ */
+ MPP.revealTruncatedText = function () {
+ if ( this.$container.hasClass( 'mw-mmv-untruncated' ) ) {
+ return;
+ }
+ this.$container.addClass( 'mw-mmv-untruncated' );
+ this.title.grow();
+ this.creditField.grow();
+ };
+
+ /**
+ * Undoes changes made by revealTruncatedText().
+ */
+ MPP.hideTruncatedText = function () {
+ if ( !this.$container.hasClass( 'mw-mmv-untruncated' ) ) {
+ return;
+ }
+ this.title.shrink();
+ this.creditField.shrink();
+ this.$container.removeClass( 'mw-mmv-untruncated' );
+ };
+
+ /**
+ * Hide or reveal truncated text based on whether the panel is open. This is normally handled by
+ * MetadataPanelScroller, but when the panel is reset (e.g. on a prev/next event) sometimes the panel position can change without a panel , such as on a
+ * prev/next event; in such cases this function has to be called.
+ */
+ MPP.resetTruncatedText = function () {
+ if ( this.scroller.panelIsOpen() ) {
+ this.revealTruncatedText();
+ } else {
+ this.hideTruncatedText();
+ }
+ };
+
+ mw.mmv.ui.MetadataPanel = MetadataPanel;
+}( mediaWiki, jQuery, OO ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.metadataPanel.less b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.metadataPanel.less
new file mode 100644
index 00000000..9d3e3911
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.metadataPanel.less
@@ -0,0 +1,392 @@
+@import '../mmv.globals';
+@import '../mmv.mixins';
+
+@panel-below-fold-background-color: #f8f9fa;
+
+@fold-separator-border-width: 1px;
+@vertical-padding: 10px;
+@horizontal-padding: 20px;
+@space-above-credit: 2px;
+
+.mw-mmv-info-box {
+ display: inline-block;
+ overflow: hidden;
+ border: 1px solid #c8ccd1;
+ border-radius: @border-radius;
+ background-color: #fff;
+}
+
+.mw-mmv-title-contain {
+ position: relative;
+}
+
+.mw-mmv-title-para {
+ @height: @metadatabar-above-fold-inner-height - 2 * @vertical-padding; // needs explicit height for text truncation logic
+ margin: 0 0 @vertical-padding; // use margin instead of padding for bottom so text is not visible
+ padding: @vertical-padding @horizontal-padding 0 @horizontal-padding;
+ height: @height;
+ line-height: @height;
+
+ &.mw-mmv-ttf-normal {
+ font-size: 20px;
+ }
+ &.mw-mmv-title-small {
+ font-size: 18px;
+ }
+ &.mw-mmv-title-smaller {
+ @vertical-padding: 6px;
+ @height: @metadatabar-above-fold-inner-height - 2 * @vertical-padding;
+ height: @height;
+ margin: 0 0 @horizontal-padding;
+ padding: @vertical-padding @horizontal-padding 0 @horizontal-padding;
+ line-height: floor( @height / 2 ); // two lines
+ font-size: 16px;
+ }
+
+ // this should be after the size-specific styles so it can override any height set there
+ &.mw-mmv-ttf-untruncated {
+ height: auto;
+ }
+
+ .mw-mmv-ttf-ellipsis {
+ right: @horizontal-padding;
+ bottom: 3px;
+ background-color: @panel-above-fold-background-color; // clip text
+
+ &:before {
+ .fade-out-horizontal( @panel-above-fold-background-color );
+ }
+ }
+}
+
+.mw-mmv-credit {
+ @padding-right: 5px;
+ margin: 0;
+ color: #54595d;
+ padding: 0 0 @padding-right;
+ font-size: 0.85em;
+
+ .metadata-panel-is-closed {
+ height: @metadatabar-below-fold-pushup-height - @space-above-credit - @fold-separator-border-width; // needs explicit height for text truncation logic
+ line-height: @metadatabar-below-fold-pushup-height - @space-above-credit - @fold-separator-border-width;
+ }
+
+ &.empty {
+ height: 0.85em;
+ }
+
+ .mw-mmv-ttf-ellipsis {
+ right: @padding-right;
+ bottom: 13px;
+ background-color: @panel-below-fold-background-color; // clip text
+
+ &:before {
+ .fade-out-horizontal( @panel-below-fold-background-color );
+ }
+ }
+}
+
+.mw-mmv-source-author {
+ line-height: 1.8em;
+}
+
+.mw-mmv-title {
+ // this element is the click target for text untruncation; with the default display:inline it would be an
+ // inline box which can have "gaps" between the lines, making clicks seem to have no effect
+ display: inline-block;
+}
+
+.mw-mmv-image-metadata {
+ width: 100%;
+ position: relative;
+ margin-top: -@metadatabar-below-fold-pushup-height;
+ border-top: @fold-separator-border-width solid #c8ccd1;
+ background-color: @panel-below-fold-background-color;
+ padding-top: @space-above-credit;
+
+ .jq-fullscreened & {
+ // Make sure content fits into the screen. This assumes no paddings.
+ height: @metadatabar-below-fold-pushup-height - @fold-separator-border-width - @space-above-credit;
+ overflow: hidden;
+ }
+ .jq-fullscreened .mw-mmv-untruncated & {
+ height: auto;
+ }
+}
+
+.mw-mmv-author:before {
+ display: inline-block;
+ vertical-align: middle;
+ height: 16px;
+ width: 16px;
+ content: ' ';
+ margin-right: 7px;
+ margin-bottom: 2px;
+ background-size: contain;
+ background-position: center center;
+ background-repeat: no-repeat;
+ /* @embed */
+ background-image: url( img/user-ltr.svg );
+}
+
+.mw-mmv-image-desc-div {
+ overflow-y: auto;
+ max-height: 150px;
+ margin-bottom: 15px;
+
+ &.empty {
+ display: none;
+ }
+}
+
+.mw-mmv-image-desc-div,
+.mw-mmv-image-links-div {
+ display: inline-block;
+ vertical-align: top;
+}
+
+@littlefont: 0.85em;
+@mediumfont: 0.95em;
+
+.mw-mmv-image-desc {
+ font-size: @mediumfont;
+ color: #54595d;
+}
+
+.mw-mmv-image-links {
+ margin: 0 20px;
+
+ li {
+ list-style: none;
+ font-size: @littlefont;
+ color: #3f4040;
+
+ &.empty {
+ display: none;
+ }
+
+ // Make sure the next list item is not visible when the
+ // metadata panel is closed
+ .metadata-panel-is-closed &.mw-mmv-license-li {
+ height: @metadatabar-below-fold-pushup-height - @space-above-credit;
+ line-height: @metadatabar-below-fold-pushup-height - @space-above-credit;
+ }
+
+ &:before {
+ display: inline-block;
+ vertical-align: middle;
+ height: 16px;
+ width: 16px;
+ content: ' ';
+ margin-right: 7px;
+ margin-bottom: 2px;
+ background-size: contain;
+ background-position: right center;
+ background-repeat: no-repeat;
+ }
+
+ &.mw-mmv-license-li:before {
+ /* @embed */
+ background-image: url( img/license.svg );
+ }
+
+ &.mw-mmv-license-li.cc-license:before {
+ /* @embed */
+ background-image: url( img/cc.svg );
+ }
+
+ &.mw-mmv-license-li.pd-license:before {
+ /* @embed */
+ background-image: url( img/pd.svg );
+ }
+
+ &.mw-mmv-filename-li:before {
+ /* @embed */
+ background-image: url( img/file.svg );
+ }
+
+ &.mw-mmv-datetime-li:before {
+ /* @embed */
+ background-image: url( img/time.svg );
+ }
+
+ &.mw-mmv-location-li:before {
+ /* @embed */
+ background-image: url( img/location.svg );
+ }
+
+ &.empty:before {
+ background-image: none !important; /* stylelint-disable-line declaration-no-important */
+ }
+ }
+}
+
+.mw-mmv-license-contain,
+.mw-mmv-license {
+ text-align: right;
+}
+
+.mw-mmv-filename-prefix {
+ padding-right: 4px;
+ cursor: text;
+}
+
+.mw-mmv-title-para,
+.mw-mmv-credit,
+.mw-mmv-image-desc {
+ padding-left: @horizontal-padding;
+}
+
+.mw-mmv-about-links {
+ font-size: @littlefont;
+ padding: @horizontal-padding;
+ width: 50%;
+ clear: both;
+}
+
+.mw-mmv-label {
+ background-color: #eaecf0;
+ color: #222;
+ margin-left: 6px;
+ margin-top: 1px;
+ border-radius: @border-radius;
+ padding: 2px 5px;
+ font-size: 0.9em;
+
+ &:hover {
+ background-color: #c8ccd1;
+ }
+}
+
+.mw-mmv-image-metadata-column {
+ float: left;
+}
+
+.mw-mmv-image-metadata-desc-column {
+ width: 66.5%;
+}
+
+.mw-mmv-image-metadata-links-column {
+ max-width: 33.5%;
+ width: 25%;
+ text-align: right;
+ float: right;
+ transition: width 0.2s ease-out;
+
+ .mw-mmv-untruncated & {
+ width: 33.5%;
+ text-align: left;
+ }
+
+}
+
+.mw-mmv-restrictions {
+ display: inline-block;
+ line-height: 14px;
+}
+
+.mw-mmv-restriction-label {
+ &,
+ &:hover {
+ background-color: #fc6;
+ }
+
+ cursor: default;
+ display: inline-block;
+ height: 16px;
+}
+
+.mw-mmv-restriction-label-inner {
+ display: inline-block;
+ width: 14px;
+ height: 14px;
+ text-indent: -9999px;
+ text-align: left;
+ background-size: contain;
+ &:after {
+ float: right;
+ text-indent: 0;
+ }
+}
+
+.mw-mmv-restriction-2257 {
+ /* @embed */
+ background-image: url( img/restrict-2257.svg );
+}
+
+.mw-mmv-restriction-aus-reserve {
+ /* @embed */
+ background-image: url( img/restrict-aus-reserve.svg );
+}
+
+.mw-mmv-restriction-communist {
+ /* @embed */
+ background-image: url( img/restrict-communist.svg );
+}
+
+.mw-mmv-restriction-costume {
+ /* @embed */
+ background-image: url( img/restrict-costume.svg );
+}
+
+.mw-mmv-restriction-currency {
+ /* @embed */
+ background-image: url( img/restrict-currency.svg );
+}
+
+.mw-mmv-restriction-design {
+ /* @embed */
+ background-image: url( img/restrict-design.svg );
+}
+
+.mw-mmv-restriction-fan-art {
+ /* @embed */
+ background-image: url( img/restrict-fan-art.svg );
+}
+
+.mw-mmv-restriction-ihl {
+ /* @embed */
+ background-image: url( img/restrict-ihl.svg );
+}
+
+.mw-mmv-restriction-insignia {
+ /* @embed */
+ background-image: url( img/restrict-insignia.svg );
+}
+
+.mw-mmv-restriction-ita-mibac {
+ /* @embed */
+ background-image: url( img/restrict-ita-mibac.svg );
+}
+
+.mw-mmv-restriction-nazi {
+ /* @embed */
+ background-image: url( img/restrict-nazi.svg );
+}
+
+.mw-mmv-restriction-personality {
+ /* @embed */
+ background-image: url( img/restrict-personality.svg );
+}
+
+.mw-mmv-restriction-trademarked:after {
+ content: '\002122';
+}
+
+.mw-mmv-restriction-default {
+ /* @embed */
+ background-image: url( img/restrict-default.svg );
+}
+
+.mw-mmv-permission-link {
+ cursor: pointer;
+
+ .jq-fullscreened & {
+ display: none;
+ }
+}
+
+.mw-mmv-optout-link.pending {
+ cursor: wait;
+ color: #54595d;
+}
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.metadataPanelScroller.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.metadataPanelScroller.js
new file mode 100644
index 00000000..d7095322
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.metadataPanelScroller.js
@@ -0,0 +1,247 @@
+/*
+ * This file is part of the MediaWiki extension MediaViewer.
+ *
+ * MediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $, oo ) {
+ var MPSP;
+
+ /**
+ * Handles scrolling behavior of the metadata panel.
+ *
+ * @class mw.mmv.ui.MetadataPanelScroller
+ * @extends mw.mmv.ui.Element
+ * @constructor
+ * @param {jQuery} $container The container for the panel (.mw-mmv-post-image).
+ * @param {jQuery} $aboveFold The control bar element (.mw-mmv-above-fold).
+ * @param {mw.storage} localStorage the localStorage object, for dependency injection
+ */
+ function MetadataPanelScroller( $container, $aboveFold, localStorage ) {
+ mw.mmv.ui.Element.call( this, $container );
+
+ this.$aboveFold = $aboveFold;
+
+ /** @property {mw.storage} localStorage */
+ this.localStorage = localStorage;
+
+ /** @property {boolean} panelWasOpen state flag which will be used to detect open <-> closed transitions */
+ this.panelWasOpen = null;
+
+ /**
+ * Whether this user has ever opened the metadata panel.
+ * Based on a localstorage flag; will be set to true if the client does not support localstorage.
+ * @type {boolean}
+ */
+ this.hasOpenedMetadata = undefined;
+
+ /**
+ * Whether we've already fired an animation for the metadata div in this lightbox session.
+ * @property {boolean}
+ * @private
+ */
+ this.hasAnimatedMetadata = false;
+
+ this.initialize();
+ }
+ oo.inheritClass( MetadataPanelScroller, mw.mmv.ui.Element );
+ MPSP = MetadataPanelScroller.prototype;
+
+ MPSP.attach = function () {
+ var panel = this;
+
+ this.handleEvent( 'keydown', function ( e ) {
+ panel.keydown( e );
+ } );
+
+ $( window ).on( 'scroll.mmvp', $.throttle( 250, function () {
+ panel.scroll();
+ } ) );
+
+ this.$container.on( 'mmv-metadata-open', function () {
+ if ( !panel.hasOpenedMetadata && panel.localStorage.store ) {
+ panel.hasOpenedMetadata = true;
+ panel.localStorage.set( 'mmv.hasOpenedMetadata', '1' );
+ }
+ } );
+
+ // reset animation flag when the viewer is reopened
+ this.hasAnimatedMetadata = false;
+ };
+
+ MPSP.unattach = function () {
+ this.clearEvents();
+ $( window ).off( 'scroll.mmvp' );
+ this.$container.off( 'mmv-metadata-open' );
+ };
+
+ MPSP.empty = function () {
+ // need to remove this to avoid animating again when reopening lightbox on same page
+ this.$container.removeClass( 'invite' );
+
+ this.panelWasOpen = this.panelIsOpen();
+ };
+
+ /**
+ * Returns scroll top position when the panel is fully open.
+ * (In other words, the height of the area that is outside the screen, in pixels.)
+ *
+ * @return {number}
+ */
+ MPSP.getScrollTopWhenOpen = function () {
+ return this.$container.outerHeight() - parseInt( this.$aboveFold.css( 'min-height' ), 10 ) -
+ parseInt( this.$aboveFold.css( 'padding-bottom' ), 10 );
+ };
+
+ /**
+ * Makes sure the panel does not contract when it is emptied and thus keeps its position as much as possible.
+ * This should be called when switching images, before the panel is emptied, and should be undone with
+ * unfreezeHeight after the panel has been populeted with the new metadata.
+ */
+ MPSP.freezeHeight = function () {
+ var scrollTop, scrollTopWhenOpen;
+
+ if ( !this.$container.is( ':visible' ) ) {
+ return;
+ }
+
+ scrollTop = $( window ).scrollTop();
+ scrollTopWhenOpen = this.getScrollTopWhenOpen();
+
+ this.panelWasFullyOpen = ( scrollTop === scrollTopWhenOpen );
+ this.$container.css( 'min-height', this.$container.height() );
+ };
+
+ MPSP.unfreezeHeight = function () {
+ if ( !this.$container.is( ':visible' ) ) {
+ return;
+ }
+
+ this.$container.css( 'min-height', '' );
+ if ( this.panelWasFullyOpen ) {
+ $( window ).scrollTop( this.getScrollTopWhenOpen() );
+ }
+ };
+
+ MPSP.initialize = function () {
+ var value = this.localStorage.get( 'mmv.hasOpenedMetadata' );
+
+ // localStorage will only store strings; if values `null`, `false` or
+ // `0` are set, they'll come out as `"null"`, `"false"` or `"0"`, so we
+ // can be certain that an actual null is a failure to locate the item,
+ // and false is an issue with localStorage itself
+ if ( value !== false ) {
+ this.hasOpenedMetadata = value !== null;
+ } else {
+ // if there was an issue with localStorage, treat it as opened
+ this.hasOpenedMetadata = true;
+ }
+ };
+
+ /**
+ * Animates the metadata area when the viewer is first opened.
+ */
+ MPSP.animateMetadataOnce = function () {
+ if ( !this.hasOpenedMetadata && !this.hasAnimatedMetadata ) {
+ this.hasAnimatedMetadata = true;
+ this.$container.addClass( 'invite' );
+ }
+ };
+
+ /**
+ * Toggles the metadata div being totally visible.
+ *
+ * @param {string} [forceDirection] 'up' or 'down' makes the panel move on that direction (and is a noop
+ * if the panel is already at the upmost/bottommost position); without the parameter, the panel position
+ * is toggled. (Partially open counts as open.)
+ * @return {jQuery.Promise} A promise which resolves after the animation has finished.
+ */
+ MPSP.toggle = function ( forceDirection ) {
+ var scrollTopWhenOpen = this.getScrollTopWhenOpen(),
+ scrollTopWhenClosed = 0,
+ scrollTop = $( window ).scrollTop(),
+ panelIsOpen = scrollTop > scrollTopWhenClosed,
+ direction = forceDirection || ( panelIsOpen ? 'down' : 'up' ),
+ scrollTopTarget = ( direction === 'up' ) ? scrollTopWhenOpen : scrollTopWhenClosed;
+
+ // don't log / animate if the panel is already in the end position
+ if ( scrollTopTarget === scrollTop ) {
+ return $.Deferred().resolve().promise();
+ } else {
+ mw.mmv.actionLogger.log( direction === 'up' ? 'metadata-open' : 'metadata-close' );
+ if ( direction === 'up' && !panelIsOpen ) {
+ // FIXME nasty. This is not really an event but a command sent to the metadata panel;
+ // child UI elements should not send commands to their parents. However, there is no way
+ // to calculate the target scrollTop accurately without revealing the text, and the event
+ // which does that (metadata-open) is only triggered later in the process, when the panel
+ // actually scrolled, so we cannot use it here without risking triggering it multiple times.
+ this.$container.trigger( 'mmv-metadata-reveal-truncated-text' );
+ scrollTopTarget = this.getScrollTopWhenOpen();
+ }
+ return $( 'html, body' ).animate( { scrollTop: scrollTopTarget }, 'fast' ).promise();
+ }
+ };
+
+ /**
+ * Handles keydown events for this element.
+ *
+ * @param {jQuery.Event} e Key down event
+ */
+ MPSP.keydown = function ( e ) {
+ if ( e.altKey || e.shiftKey || e.ctrlKey || e.metaKey ) {
+ return;
+ }
+ switch ( e.which ) {
+ case 40: // Down arrow
+ // fall through
+ case 38: // Up arrow
+ this.toggle();
+ e.preventDefault();
+ break;
+ }
+ };
+
+ /**
+ * Returns whether the metadata panel is open. (Partially open is considered to be open.)
+ *
+ * @return {boolean}
+ */
+ MPSP.panelIsOpen = function () {
+ return $( window ).scrollTop() > 0;
+ };
+
+ /**
+ * Receives the window's scroll events and and turns them into business logic events
+ *
+ * @fires mmv-metadata-open
+ * @fires mmv-metadata-close
+ */
+ MPSP.scroll = function () {
+ var panelIsOpen = this.panelIsOpen();
+
+ if ( panelIsOpen && !this.panelWasOpen ) { // just opened
+ this.$container.trigger( 'mmv-metadata-open' );
+ // This will include keyboard- and mouseclick-initiated open events as well,
+ // since the panel is anomated, which counts as scrolling.
+ // Filtering these seems too much trouble to be worth it.
+ mw.mmv.actionLogger.log( 'metadata-scroll-open' );
+ } else if ( !panelIsOpen && this.panelWasOpen ) { // just closed
+ this.$container.trigger( 'mmv-metadata-close' );
+ mw.mmv.actionLogger.log( 'metadata-scroll-close' );
+ }
+ this.panelWasOpen = panelIsOpen;
+ };
+
+ mw.mmv.ui.MetadataPanelScroller = MetadataPanelScroller;
+}( mediaWiki, jQuery, OO ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.metadataPanelScroller.less b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.metadataPanelScroller.less
new file mode 100644
index 00000000..4edf59f6
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.metadataPanelScroller.less
@@ -0,0 +1,69 @@
+@import '../mmv.globals';
+@import '../mmv.mixins';
+@import 'mediawiki.mixins.animation';
+
+.mw-mmv-post-image {
+ .animation( mw-mmv-appear-animation 0.5s ease 0s 1 normal forwards );
+ transition: box-shadow 0.25s;
+
+ &.invite {
+ .animation( mw-mmv-invite-animation 0.9s ease 0.2s 1 normal forwards );
+ }
+
+ .jq-fullscreened & {
+ .animation( none );
+ }
+
+ &.mw-mmv-untruncated,
+ .jq-fullscreened & {
+ box-shadow: 0 -4px 0 rgba( 0, 0, 0, 0.2 );
+ }
+}
+
+.mw-mmv-appear-animation() {
+ 0% {
+ opacity: 0.6;
+ }
+ 50% {
+ opacity: 0.9;
+ }
+ 100% {
+ opacity: 1;
+ }
+}
+
+@-webkit-keyframes mw-mmv-appear-animation {
+ .mw-mmv-appear-animation;
+}
+
+@-moz-keyframes mw-mmv-appear-animation {
+ .mw-mmv-appear-animation;
+}
+
+@keyframes mw-mmv-appear-animation {
+ .mw-mmv-appear-animation;
+}
+
+.mw-mmv-invite-animation() {
+ 0% {
+ margin-top: 0;
+ }
+ 30% {
+ margin-top: -15px;
+ }
+ 85% {
+ margin-top: 0;
+ }
+}
+
+@-webkit-keyframes mw-mmv-invite-animation {
+ .mw-mmv-invite-animation;
+}
+
+@-moz-keyframes mw-mmv-invite-animation {
+ .mw-mmv-invite-animation;
+}
+
+@keyframes mw-mmv-invite-animation {
+ .mw-mmv-invite-animation;
+}
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.permission.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.permission.js
new file mode 100644
index 00000000..74ce639a
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.permission.js
@@ -0,0 +1,173 @@
+/*
+ * This file is part of the MediaWiki extension MediaViewer.
+ *
+ * MediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $, oo ) {
+ var P;
+
+ /**
+ * A box to display additional terms or remarks from the image author.
+ * (Typically comes from the Permission field of the {{Information}} template.)
+ * It has two states: when closed, it just shows some text, when open, it shows the HTML
+ * block supplied by the author in its full beauty.
+ *
+ * @class mw.mmv.ui.Permission
+ * @extends mw.mmv.ui.Element
+ * @constructor
+ * @param {jQuery} $container
+ * @param {mw.mmv.ui.MetadataPanelScroller} scroller
+ */
+ function Permission( $container, scroller ) {
+ var permission = this;
+
+ mw.mmv.ui.Element.call( this, $container );
+
+ /** @property {mw.mmv.HtmlUtils} htmlUtils - */
+ this.htmlUtils = new mw.mmv.HtmlUtils();
+
+ /**
+ * Contains everything else.
+ * @property {jQuery}
+ */
+ this.$box = $( '<div>' )
+ .addClass( 'mw-mmv-permission-box mw-mmv-info-box empty' )
+ .appendTo( this.$container );
+
+ /**
+ * Box title
+ * @property {jQuery}
+ */
+ this.$title = $( '<h3>' )
+ .text( mw.message( 'multimediaviewer-permission-title' ).text() )
+ .appendTo( this.$box );
+
+ /**
+ * Plain-text version of the author's message
+ * This is just the text parsed out from the original markup, it might not make much sense
+ * (e.g. if the original is a HTML table)
+ * @property {jQuery}
+ */
+ this.$text = $( '<div>' )
+ .addClass( 'mw-mmv-permission-text' )
+ .appendTo( this.$box )
+ .on( 'click', '.mw-mmv-permission-text-viewmore', function ( e ) {
+ e.preventDefault();
+ permission.grow();
+ permission.scroller.toggle( 'up' );
+ } )
+ ;
+
+ /**
+ * A helper element to fade off text
+ * @property {jQuery}
+ */
+ this.$fader = $( '<div>' )
+ .addClass( 'mw-mmv-permission-text-fader' )
+ .append(
+ $( '<a>' )
+ .addClass( 'mw-mmv-permission-text-viewmore' )
+ .prop( 'href', '#' )
+ .text( mw.message( 'multimediaviewer-permission-viewmore' ).text() )
+ );
+
+ /**
+ * Original (HTML) version of the author's message
+ * This can be scary sometimes (huge tables, black text on dark purple background etc).
+ * @property {jQuery}
+ */
+ this.$html = $( '<div>' )
+ .addClass( 'mw-mmv-permission-html' )
+ .appendTo( this.$box );
+
+ /**
+ * "Close" button (does not actually close the box, just makes it smaller).
+ * @property {jQuery}
+ */
+ this.$close = $( '<button>' )
+ .addClass( 'mw-mmv-permission-close' )
+ .on( 'click', function () {
+ permission.shrink();
+ } )
+ .appendTo( this.$box );
+
+ /**
+ * Panel scroller from the metadata panel object.
+ * @property {mw.mmv.ui.MetadataPanelScroller}
+ */
+ this.scroller = scroller;
+ }
+ oo.inheritClass( Permission, mw.mmv.ui.Element );
+ P = Permission.prototype;
+
+ /**
+ * Clear everything
+ */
+ P.empty = function () {
+ this.$box.addClass( 'empty' );
+ this.$text.empty();
+ this.$html.empty();
+ };
+
+ /**
+ * Set permission text/html
+ *
+ * @param {string} permission the text or HTML code written by the image author
+ */
+ P.set = function ( permission ) {
+ this.$box.removeClass( 'empty' );
+
+ this.$text.html( this.htmlUtils.htmlToTextWithLinks( permission ) );
+ this.$text.append( this.$fader );
+
+ this.$html.html( permission );
+ };
+
+ /**
+ * Enlarge the box, show HTML instead of text.
+ *
+ * @fires mmv-permission-grow
+ */
+ P.grow = function () {
+ mw.mmv.actionLogger.log( 'terms-open' );
+
+ this.$box.addClass( 'full-size' )
+ .stop( true )
+ .animate( { backgroundColor: '#FFFFA0' }, 500 )
+ .animate( { backgroundColor: '#FFFFFF' }, 500 );
+ this.$container.trigger( 'mmv-permission-grow' );
+ };
+
+ /**
+ * Limit the size of the box, show text only.
+ *
+ * @fires mmv-permission-shrink
+ */
+ P.shrink = function () {
+ this.$box.removeClass( 'full-size' );
+ this.$container.trigger( 'mmv-permission-shrink' );
+ };
+
+ /**
+ * Returns whether the box is full-size.
+ *
+ * @return {boolean}
+ */
+ P.isFullSize = function () {
+ return this.$box.hasClass( 'full-size' );
+ };
+
+ mw.mmv.ui.Permission = Permission;
+}( mediaWiki, jQuery, OO ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.permission.less b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.permission.less
new file mode 100644
index 00000000..52051594
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.permission.less
@@ -0,0 +1,81 @@
+@import 'mediawiki.mixins';
+@import '../mmv.mixins';
+
+.mw-mmv-permission-box {
+ position: relative;
+ width: 90%;
+ margin: 10px 20px 0;
+
+ &.empty {
+ display: none;
+ }
+
+ h3 {
+ margin: 10px;
+ padding: 0;
+ color: #54595d;
+ font-size: 0.95em;
+ }
+
+ .mw-mmv-permission-close {
+ display: none;
+ position: absolute;
+ top: 12px;
+ right: 8px;
+ width: 16px;
+ height: 16px;
+ .background-image( 'img/x_gray.svg' );
+ cursor: pointer;
+ }
+
+ .mw-mmv-permission-text {
+ @text-font-size: 0.9em;
+ @text-line-height: 1.4;
+ @lines-shown: 3;
+
+ position: relative;
+ max-height: @lines-shown * @text-line-height * @text-font-size;
+ overflow: hidden;
+ margin: 0 10px 10px;
+ font-size: @text-font-size;
+ line-height: @text-line-height;
+ color: #54595d;
+
+ .mw-mmv-permission-text-fader {
+ position: absolute;
+ top: ( @lines-shown - 1 ) * @text-line-height * @text-font-size;
+ width: 100%;
+ height: @text-line-height * @text-font-size;
+ .fade-out-vertical();
+ text-align: right;
+
+ a {
+ padding: 3px 0 0 1em;
+ background-color: #fff;
+ font-size: 1em;
+ }
+ }
+ }
+
+ .mw-mmv-permission-html {
+ padding: 0 15px 15px;
+ display: none;
+ max-height: 400px;
+ overflow: auto;
+ }
+
+ &.full-size {
+ .mw-mmv-permission-close {
+ display: block;
+ }
+
+ .mw-mmv-permission-text {
+ display: none;
+ }
+
+ .mw-mmv-permission-html {
+ display: block;
+ border-top: 1px solid #f8f9fa;
+ }
+ }
+}
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.progressBar.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.progressBar.js
new file mode 100644
index 00000000..c34a27d4
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.progressBar.js
@@ -0,0 +1,93 @@
+/*
+ * This file is part of the MediaWiki extension MediaViewer.
+ *
+ * MediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $, oo ) {
+ var PBP;
+
+ /**
+ * A progress bar for the loading of the image.
+ *
+ * @class mw.mmv.ui.ProgressBar
+ * @extends mw.mmv.ui.Element
+ * @constructor
+ * @param {jQuery} $container
+ */
+ function ProgressBar( $container ) {
+ mw.mmv.ui.Element.call( this, $container );
+ this.init();
+ }
+ oo.inheritClass( ProgressBar, mw.mmv.ui.Element );
+ PBP = ProgressBar.prototype;
+
+ /**
+ * Initializes the progress display at the top of the panel.
+ */
+ PBP.init = function () {
+ this.$progress = $( '<div>' )
+ .addClass( 'mw-mmv-progress empty' )
+ .appendTo( this.$container );
+ this.$percent = $( '<div>' )
+ .addClass( 'mw-mmv-progress-percent' )
+ .appendTo( this.$progress );
+ };
+
+ PBP.empty = function () {
+ this.hide();
+ };
+
+ /**
+ * Hides the bar, resets it to 0 and stops any animation in progress.
+ */
+ PBP.hide = function () {
+ this.$progress.addClass( 'empty' );
+ this.$percent.stop().css( { width: 0 } );
+ };
+
+ /**
+ * Handles the progress display when a percentage of progress is received
+ *
+ * @param {number} percent a number between 0 and 100
+ */
+ PBP.animateTo = function ( percent ) {
+ var panel = this;
+
+ this.$progress.removeClass( 'empty' );
+ this.$percent.stop();
+
+ if ( percent === 100 ) {
+ // When a 100% update comes in, we make sure that the bar is visible, we animate
+ // fast to 100 and we hide the bar when the animation is done
+ this.$percent.animate( { width: percent + '%' }, 50, 'swing', $.proxy( panel.hide, panel ) );
+ } else {
+ // When any other % update comes in, we make sure the bar is visible
+ // and we animate to the right position
+ this.$percent.animate( { width: percent + '%' } );
+ }
+ };
+
+ /**
+ * Goes to the given percent without animation
+ *
+ * @param {number} percent a number between 0 and 100
+ */
+ PBP.jumpTo = function ( percent ) {
+ this.$progress.removeClass( 'empty' );
+ this.$percent.stop().css( { width: percent + '%' } );
+ };
+
+ mw.mmv.ui.ProgressBar = ProgressBar;
+}( mediaWiki, jQuery, OO ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.progressBar.less b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.progressBar.less
new file mode 100644
index 00000000..9f4c2a67
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.progressBar.less
@@ -0,0 +1,46 @@
+@import '../mmv.globals';
+@import 'mediawiki.mixins.animation';
+
+@progress-height: @progress-bar-height;
+
+.mw-mmv-progress {
+ background-color: #c8ccd1;
+ background-color: rgba( 221, 221, 221, 0.5 );
+ width: 100%;
+ height: @progress-height;
+ position: absolute;
+ top: -@progress-height;
+}
+
+.mw-mmv-progress.empty {
+ display: none;
+}
+
+.mw-mmv-progress-percent {
+ width: 0;
+ height: @progress-height;
+ background: linear-gradient( -45deg, transparent 33%, rgba( 0, 0, 0, 0.1 ) 33%, rgba( 0, 0, 0, 0.1 ) 66%, transparent 66% ), #36c;
+ background-size: 35px 20px, 100% 100%, 100% 100%;
+ .animation( mw-mmv-progress-percent-animation 1.5s linear infinite );
+}
+
+.mw-mmv-progress-percent-animation() {
+ 0% {
+ background-position: 0 0;
+ }
+ 100% {
+ background-position: -70px 0;
+ }
+}
+
+@-webkit-keyframes mw-mmv-progress-percent-animation {
+ .mw-mmv-progress-percent-animation;
+}
+
+@-moz-keyframes mw-mmv-progress-percent-animation {
+ .mw-mmv-progress-percent-animation;
+}
+
+@keyframes mw-mmv-progress-percent-animation {
+ .mw-mmv-progress-percent-animation;
+}
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.reuse.dialog.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.reuse.dialog.js
new file mode 100644
index 00000000..11eb46f4
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.reuse.dialog.js
@@ -0,0 +1,264 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $, oo ) {
+ // Shortcut for prototype later
+ var DP;
+
+ /**
+ * Represents the file reuse dialog and the link to open it.
+ *
+ * @class mw.mmv.ui.reuse.Dialog
+ * @extends mw.mmv.ui.Element
+ * @param {jQuery} $container the element to which the dialog will be appended
+ * @param {jQuery} $openButton the button which opens the dialog. Only used for positioning.
+ * @param {mw.mmv.Config} config
+ */
+ function Dialog( $container, $openButton, config ) {
+ mw.mmv.ui.Dialog.call( this, $container, $openButton, config );
+
+ /**
+ * @property {Object.<string, mw.mmv.ui.Element>} tabs List of tab ui objects.
+ */
+ this.tabs = null;
+
+ /**
+ * @property {Object.<string, OO.ui.MenuOptionWidget>} ooTabs List of tab OOUI objects.
+ */
+ this.ooTabs = null;
+
+ this.loadDependencies.push( 'mmv.ui.reuse.shareembed' );
+
+ this.$dialog.addClass( 'mw-mmv-reuse-dialog' );
+
+ this.eventPrefix = 'use-this-file';
+ }
+
+ oo.inheritClass( Dialog, mw.mmv.ui.Dialog );
+ DP = Dialog.prototype;
+
+ // FIXME this should happen outside the dialog and the tabs, but we need to improve
+ DP.initTabs = function () {
+ function makeTab( type ) {
+ return new oo.ui.MenuOptionWidget( {
+ data: type,
+ label: mw.message( 'multimediaviewer-' + type + '-tab' ).text()
+ } );
+ }
+
+ this.reuseTabs = new oo.ui.MenuSelectWidget( {
+ autoHide: false,
+ classes: [ 'mw-mmv-reuse-tabs' ]
+ } );
+ this.reuseTabs.$element.appendTo( this.$dialog );
+
+ this.tabs = {
+ share: new mw.mmv.ui.reuse.Share( this.$dialog ),
+ embed: new mw.mmv.ui.reuse.Embed( this.$dialog )
+ };
+
+ this.ooTabs = {
+ share: makeTab( 'share' ),
+ embed: makeTab( 'embed' )
+ };
+
+ this.reuseTabs.addItems( [
+ this.ooTabs.share,
+ this.ooTabs.embed
+ ] );
+
+ // MenuSelectWidget has a nasty tendency to hide itself, maybe we're not using it right?
+ this.reuseTabs.toggle( true );
+ this.reuseTabs.toggle = $.noop;
+
+ this.selectedTab = this.getLastUsedTab();
+
+ // In case nothing is saved in localStorage or it contains junk
+ if ( !this.tabs.hasOwnProperty( this.selectedTab ) ) {
+ this.selectedTab = 'share';
+ }
+
+ this.reuseTabs.selectItem( this.ooTabs[ this.selectedTab ] );
+
+ if ( this.dependenciesNeedToBeAttached ) {
+ this.attachDependencies();
+ }
+
+ if ( this.tabsSetValues ) {
+ // This is a delayed set() for the elements we've just created on demand
+ this.tabs.share.set.apply( this.tabs.share, this.tabsSetValues.share );
+ this.tabs.embed.set.apply( this.tabs.embed, this.tabsSetValues.embed );
+ this.showImageWarnings( this.tabsSetValues.share[ 0 ] );
+ this.tabsSetValues = undefined;
+ }
+ };
+
+ DP.toggleDialog = function () {
+ if ( this.tabs === null ) {
+ this.initTabs();
+ }
+
+ mw.mmv.ui.Dialog.prototype.toggleDialog.call( this );
+ };
+
+ /**
+ * Handles tab selection.
+ *
+ * @param {OO.ui.MenuOptionWidget} option
+ */
+ DP.handleTabSelection = function ( option ) {
+ var tab;
+
+ this.selectedTab = option.getData();
+
+ for ( tab in this.tabs ) {
+ if ( tab === this.selectedTab ) {
+ this.tabs[ tab ].show();
+ } else {
+ this.tabs[ tab ].hide();
+ }
+ }
+
+ this.config.setInLocalStorage( 'mmv-lastUsedTab', this.selectedTab );
+ };
+
+ /**
+ * @return {string} Last used tab
+ */
+ DP.getLastUsedTab = function () {
+ return this.config.getFromLocalStorage( 'mmv-lastUsedTab' );
+ };
+
+ /**
+ * Registers listeners.
+ */
+ DP.attach = function () {
+ this.handleEvent( 'mmv-reuse-open', $.proxy( this.handleOpenCloseClick, this ) );
+
+ this.handleEvent( 'mmv-download-open', $.proxy( this.closeDialog, this ) );
+ this.handleEvent( 'mmv-options-open', $.proxy( this.closeDialog, this ) );
+
+ this.attachDependencies();
+ };
+
+ /**
+ * Registrers listeners for dependencies loaded on demand
+ */
+ DP.attachDependencies = function () {
+ var tab, dialog = this;
+
+ if ( this.reuseTabs && this.tabs ) {
+ // This is a delayed attach() for the elements we've just created on demand
+ this.reuseTabs.on( 'select', $.proxy( dialog.handleTabSelection, dialog ) );
+
+ for ( tab in this.tabs ) {
+ this.tabs[ tab ].attach();
+ }
+
+ this.dependenciesNeedToBeAttached = false;
+ } else {
+ this.dependenciesNeedToBeAttached = true;
+ }
+ };
+
+ /**
+ * Clears listeners.
+ */
+ DP.unattach = function () {
+ var tab;
+
+ mw.mmv.ui.Dialog.prototype.unattach.call( this );
+
+ if ( this.reuseTabs ) {
+ this.reuseTabs.off( 'select' );
+ }
+
+ if ( this.tabs ) {
+ for ( tab in this.tabs ) {
+ this.tabs[ tab ].unattach();
+ }
+ }
+ };
+
+ /**
+ * Sets data needed by contaned tabs and makes dialog launch link visible.
+ *
+ * @param {mw.mmv.model.Image} image
+ * @param {mw.mmv.model.Repo} repo
+ * @param {string} caption
+ * @param {string} alt
+ */
+ DP.set = function ( image, repo, caption, alt ) {
+ if ( this.tabs !== null ) {
+ this.tabs.share.set( image );
+ this.tabs.embed.set( image, repo, caption, alt );
+ this.tabs.embed.set( image, repo, caption );
+ this.showImageWarnings( image );
+ } else {
+ this.tabsSetValues = {
+ share: [ image ],
+ embed: [ image, repo, caption, alt ]
+ };
+ }
+ };
+
+ /**
+ * @inheritdoc
+ */
+ DP.empty = function () {
+ var tab;
+
+ mw.mmv.ui.Dialog.prototype.empty.call( this );
+
+ for ( tab in this.tabs ) {
+ this.tabs[ tab ].empty();
+ }
+ };
+
+ /**
+ * @event mmv-reuse-opened
+ * Fired when the dialog is opened.
+ */
+ /**
+ * Opens a dialog with information about file reuse.
+ */
+ DP.openDialog = function () {
+ mw.mmv.ui.Dialog.prototype.openDialog.call( this );
+
+ // move warnings after the tabs
+ this.$warning.insertAfter( this.reuseTabs.$element );
+
+ this.tabs[ this.selectedTab ].show();
+
+ $( document ).trigger( 'mmv-reuse-opened' );
+ };
+
+ /**
+ * @event mmv-reuse-closed
+ * Fired when the dialog is closed.
+ */
+ /**
+ * Closes the reuse dialog.
+ */
+ DP.closeDialog = function () {
+ mw.mmv.ui.Dialog.prototype.closeDialog.call( this );
+
+ $( document ).trigger( 'mmv-reuse-closed' );
+ };
+
+ mw.mmv.ui.reuse.Dialog = Dialog;
+}( mediaWiki, jQuery, OO ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.reuse.dialog.less b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.reuse.dialog.less
new file mode 100644
index 00000000..976a2903
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.reuse.dialog.less
@@ -0,0 +1,72 @@
+@import '../mmv.mixins';
+@import '../mmv.globals';
+
+.mw-mmv-reuse-dialog {
+ @divider-border-height: 1px;
+
+ // resetting height (to overwrite .mw-mmv-dialog's bigger height), since the
+ // height may have to change for warnings being added
+ height: initial;
+ // set a standard min-height that should be larger than both panels in
+ // normal circumstances (without warnings) so that the height of the dialog
+ // doesn't change when switching tabs
+ min-height: 300px;
+ // positioned relative to the reuse button
+ bottom: @metadatabar-above-fold-height + @progress-bar-height - 5px;
+
+ .mw-mmv-reuse-tabs {
+ @divider-color: #c8ccd1;
+
+ position: static;
+ box-shadow: none;
+ padding-bottom: 0; // OOUI override
+ border: 0; // OOUI override
+ border-bottom: @divider-border-height solid @divider-color;
+ border-radius: @border-radius @border-radius 0 0;
+
+ .oo-ui-iconedElement-icon.oo-ui-icon-check,
+ .oo-ui-iconElement-icon.oo-ui-icon-check {
+ display: none;
+ }
+
+ .oo-ui-optionWidget {
+ @tab-border-height: 3px;
+ @highlighted-tab-color: #e1f3ff;
+ @selected-tab-color: #2a4b8d;
+
+ display: inline-block;
+ padding: 10px 25px;
+ font-size: 1.2em;
+
+ &.oo-ui-optionWidget-highlighted {
+ border-bottom: ( @tab-border-height - @divider-border-height ) solid @highlighted-tab-color;
+ }
+
+ &.oo-ui-optionWidget-selected,
+ &.oo-ui-optionWidget-highlighted.oo-ui-optionWidget-selected {
+ border-bottom: @tab-border-height solid @selected-tab-color;
+ }
+
+ &:first-child {
+ border-radius: @border-radius 0 0 0;
+ }
+ }
+ }
+
+ .mw-mmv-reuse-pane {
+ display: none;
+ padding: 20px;
+
+ &.active {
+ display: block;
+ }
+ }
+
+ .mw-mmv-dialog-down-arrow {
+ bottom: @metadatabar-above-fold-height + @progress-bar-height + 5px;
+ }
+
+ .mw-mmv-dialog-warning {
+ padding: 15px 30px;
+ }
+}
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.reuse.embed.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.reuse.embed.js
new file mode 100644
index 00000000..bdf03aff
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.reuse.embed.js
@@ -0,0 +1,542 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $, oo ) {
+ // Shortcut for prototype later
+ var EP;
+
+ /**
+ * UI component that provides the user html/wikitext snippets needed to share
+ * and/or embed a media asset.
+ *
+ * @class mw.mmv.ui.reuse.Embed
+ * @extends mw.mmv.ui.reuse.Tab
+ * @constructor
+ * @param {jQuery} $container
+ */
+ function Embed( $container ) {
+ mw.mmv.ui.reuse.Tab.call( this, $container );
+
+ /**
+ * Formatter converting image data into formats needed for output
+ *
+ * @property {mw.mmv.EmbedFileFormatter}
+ */
+ this.formatter = new mw.mmv.EmbedFileFormatter();
+
+ /** @property {mw.mmv.ui.Utils} utils - */
+ this.utils = new mw.mmv.ui.Utils();
+
+ /**
+ * Indicates whether or not the default option has been reset for both size menus.
+ *
+ * @property {boolean}
+ */
+ this.isSizeMenuDefaultReset = false;
+
+ this.$pane.addClass( 'mw-mmv-embed-pane' );
+
+ this.$pane.appendTo( this.$container );
+
+ this.createSnippetTextAreas( this.$pane );
+
+ this.$explanation = $( '<div>' )
+ .addClass( 'mw-mmv-shareembed-explanation mw-mmv-embed-explanation' )
+ .text( mw.message( 'multimediaviewer-embed-explanation' ).text() )
+ .appendTo( this.$pane );
+
+ this.createSnippetSelectionButtons( this.$pane );
+ this.createSizePulldownMenus( this.$pane );
+
+ /**
+ * Currently selected embed snippet.
+ *
+ * @property {jQuery}
+ */
+ this.$currentMainEmbedText = mw.user.isAnon() ? this.embedTextHtml.$element : this.embedTextWikitext.$element;
+
+ /**
+ * Default item for the html size menu.
+ *
+ * @property {OO.ui.MenuOptionWidget}
+ */
+ this.defaultHtmlItem = this.embedSizeSwitchHtml.getMenu().findSelectedItem();
+
+ /**
+ * Default item for the wikitext size menu.
+ *
+ * @property {OO.ui.MenuOptionWidget}
+ */
+ this.defaultWikitextItem = this.embedSizeSwitchWikitext.getMenu().findSelectedItem();
+
+ /**
+ * Currently selected size menu.
+ *
+ * @property {OO.ui.MenuSelectWidget}
+ */
+ this.currentSizeMenu = mw.user.isAnon() ? this.embedSizeSwitchHtml.getMenu() : this.embedSizeSwitchWikitext.getMenu();
+
+ /**
+ * Current default item.
+ *
+ * @property {OO.ui.MenuOptionWidget}
+ */
+ this.currentDefaultItem = mw.user.isAnon() ? this.defaultHtmlItem : this.defaultWikitextItem;
+ }
+ oo.inheritClass( Embed, mw.mmv.ui.reuse.Tab );
+ EP = Embed.prototype;
+
+ /** @property {number} Width threshold at which an image is to be considered "large" */
+ EP.LARGE_IMAGE_WIDTH_THRESHOLD = 1200;
+
+ /** @property {number} Height threshold at which an image is to be considered "large" */
+ EP.LARGE_IMAGE_HEIGHT_THRESHOLD = 900;
+
+ /**
+ * Creates text areas for html and wikitext snippets.
+ *
+ * @param {jQuery} $container
+ */
+ EP.createSnippetTextAreas = function ( $container ) {
+ var wikitextClasses = [ 'mw-mmv-embed-text-wikitext' ],
+ htmlClasses = [ 'mw-mmv-embed-text-html' ];
+
+ ( mw.user.isAnon() ? htmlClasses : wikitextClasses ).push( 'active' );
+
+ this.embedTextHtml = new oo.ui.MultilineTextInputWidget( {
+ classes: htmlClasses,
+ readOnly: true
+ } );
+
+ this.embedTextHtml.$element.find( 'textarea' )
+ .prop( 'placeholder', mw.message( 'multimediaviewer-reuse-loading-placeholder' ).text() );
+
+ this.embedTextHtml.$input.on( 'copy', function () {
+ mw.mmv.actionLogger.log( 'embed-html-copied' );
+ } );
+
+ this.embedTextWikitext = new oo.ui.MultilineTextInputWidget( {
+ classes: wikitextClasses,
+ readOnly: true
+ } );
+
+ this.embedTextWikitext.$element.find( 'textarea' )
+ .prop( 'placeholder', mw.message( 'multimediaviewer-reuse-loading-placeholder' ).text() );
+
+ this.embedTextWikitext.$input.on( 'copy', function () {
+ mw.mmv.actionLogger.log( 'embed-wikitext-copied' );
+ } );
+
+ this.$copyButton = $( '<button>' )
+ .addClass( 'mw-mmv-button mw-mmv-dialog-copy' )
+ .click( function () {
+ // Select the text, and then try to copy the text.
+ // If the copy fails or is not supported, continue as if nothing had happened.
+ $( this ).parent().find( '.active > textarea' ).select();
+ try {
+ if ( document.queryCommandSupported &&
+ document.queryCommandSupported( 'copy' ) ) {
+ document.execCommand( 'copy' );
+ }
+ } catch ( e ) {
+ // queryCommandSupported in Firefox pre-41 can throw errors when used with
+ // clipboard commands. We catch and ignore these and other copy-command-related
+ // errors here.
+ }
+ } )
+ .prop( 'title', mw.msg( 'multimediaviewer-reuse-copy-embed' ) )
+ .text( mw.msg( 'multimediaviewer-reuse-copy-embed' ) )
+ .tipsy( {
+ delayIn: mw.config.get( 'wgMultimediaViewer' ).tooltipDelay,
+ gravity: this.correctEW( 'se' )
+ } );
+
+ $( '<p>' )
+ .append(
+ this.embedTextHtml.$element,
+ this.embedTextWikitext.$element,
+ this.$copyButton
+ )
+ .appendTo( $container );
+ };
+
+ /**
+ * Creates snippet selection buttons.
+ *
+ * @param {jQuery} $container
+ */
+ EP.createSnippetSelectionButtons = function ( $container ) {
+ var wikitextButtonOption,
+ htmlButtonOption;
+
+ this.embedSwitch = new oo.ui.ButtonSelectWidget( {
+ classes: [ 'mw-mmv-embed-select' ]
+ } );
+
+ wikitextButtonOption = new oo.ui.ButtonOptionWidget( {
+ data: 'wikitext',
+ label: mw.message( 'multimediaviewer-embed-wt' ).text()
+ } );
+ htmlButtonOption = new oo.ui.ButtonOptionWidget( {
+ data: 'html',
+ label: mw.message( 'multimediaviewer-embed-html' ).text()
+ } );
+
+ this.embedSwitch.addItems( [
+ wikitextButtonOption,
+ htmlButtonOption
+ ] );
+
+ $( '<p>' )
+ .append( this.embedSwitch.$element )
+ .appendTo( $container );
+
+ // Logged-out defaults to 'html', logged-in to 'wikitext'
+ this.embedSwitch.selectItem( mw.user.isAnon() ? htmlButtonOption : wikitextButtonOption );
+ };
+
+ /**
+ * Creates pulldown menus to select file sizes.
+ *
+ * @param {jQuery} $container
+ */
+ EP.createSizePulldownMenus = function ( $container ) {
+ var wikitextClasses = [ 'mw-mmv-embed-size' ],
+ htmlClasses = [ 'mw-mmv-embed-size' ];
+
+ ( mw.user.isAnon() ? htmlClasses : wikitextClasses ).push( 'active' );
+
+ // Wikitext sizes pulldown menu
+ this.embedSizeSwitchWikitext = this.utils.createPulldownMenu(
+ [ 'default', 'small', 'medium', 'large' ],
+ wikitextClasses,
+ 'default'
+ );
+
+ this.embedSizeSwitchWikitext.getMenu().on( 'select', function ( item ) {
+ mw.mmv.actionLogger.log( 'embed-select-menu-wikitext-' + item.data.name );
+ } );
+
+ // Html sizes pulldown menu
+ this.embedSizeSwitchHtml = this.utils.createPulldownMenu(
+ [ 'small', 'medium', 'large', 'original' ],
+ htmlClasses,
+ 'original'
+ );
+
+ this.embedSizeSwitchHtml.getMenu().on( 'select', function ( item ) {
+ mw.mmv.actionLogger.log( 'embed-select-menu-html-' + item.data.name );
+ } );
+
+ $( '<p>' )
+ .append(
+ this.embedSizeSwitchHtml.$element,
+ this.embedSizeSwitchWikitext.$element
+ )
+ .appendTo( $container );
+ };
+
+ /**
+ * Registers listeners.
+ */
+ EP.attach = function () {
+ var embed = this,
+ $htmlTextarea = this.embedTextHtml.$element.find( 'textarea' ),
+ $wikitextTextarea = this.embedTextWikitext.$element.find( 'textarea' );
+
+ // Select all text once element gets focus
+ $htmlTextarea.on( 'focus', this.selectAllOnEvent );
+ $wikitextTextarea.on( 'focus', this.selectAllOnEvent );
+ // Disable partial text selection inside the textboxes
+ $htmlTextarea.on( 'mousedown click', this.onlyFocus );
+ $wikitextTextarea.on( 'mousedown click', this.onlyFocus );
+
+ // Register handler for switching between wikitext/html snippets
+ this.embedSwitch.on( 'select', $.proxy( embed.handleTypeSwitch, embed ) );
+
+ // Register handlers for switching between file sizes
+ this.embedSizeSwitchHtml.getMenu().on( 'choose', $.proxy( this.handleSizeSwitch, this ) );
+ this.embedSizeSwitchWikitext.getMenu().on( 'choose', $.proxy( this.handleSizeSwitch, this ) );
+ };
+
+ /**
+ * Clears listeners.
+ */
+ EP.unattach = function () {
+ var $htmlTextarea = this.embedTextHtml.$element.find( 'textarea' ),
+ $wikitextTextarea = this.embedTextWikitext.$element.find( 'textarea' );
+
+ mw.mmv.ui.reuse.Tab.prototype.unattach.call( this );
+
+ $htmlTextarea.off( 'focus mousedown click' );
+ $wikitextTextarea.off( 'focus mousedown click' );
+ this.embedSwitch.off( 'select' );
+ this.embedSizeSwitchHtml.getMenu().off( 'choose' );
+ this.embedSizeSwitchWikitext.getMenu().off( 'choose' );
+ };
+
+ /**
+ * Handles size menu change events.
+ *
+ * @param {OO.ui.MenuOptionWidget} item
+ */
+ EP.handleSizeSwitch = function ( item ) {
+ var value = item.getData();
+
+ this.changeSize( value.width, value.height );
+ };
+
+ /**
+ * Handles snippet type switch.
+ *
+ * @param {OO.ui.MenuOptionWidget} item
+ */
+ EP.handleTypeSwitch = function ( item ) {
+ var value = item.getData();
+
+ mw.mmv.actionLogger.log( 'embed-switched-to-' + value );
+
+ if ( value === 'html' ) {
+ this.$currentMainEmbedText = this.embedTextHtml.$element;
+ this.embedSizeSwitchWikitext.getMenu().toggle( false );
+
+ this.currentSizeMenu = this.embedSizeSwitchHtml.getMenu();
+ this.currentDefaultItem = this.defaultHtmlItem;
+ } else if ( value === 'wikitext' ) {
+ this.$currentMainEmbedText = this.embedTextWikitext.$element;
+ this.embedSizeSwitchHtml.getMenu().toggle( false );
+
+ this.currentSizeMenu = this.embedSizeSwitchWikitext.getMenu();
+ this.currentDefaultItem = this.defaultWikitextItem;
+ }
+
+ this.embedTextHtml.$element
+ .add( this.embedSizeSwitchHtml.$element )
+ .toggleClass( 'active', value === 'html' );
+
+ this.embedTextWikitext.$element
+ .add( this.embedSizeSwitchWikitext.$element )
+ .toggleClass( 'active', value === 'wikitext' );
+
+ // Reset current selection to default when switching the first time
+ if ( !this.isSizeMenuDefaultReset ) {
+ this.resetCurrentSizeMenuToDefault();
+ this.isSizeMenuDefaultReset = true;
+ }
+
+ this.select();
+ };
+
+ /**
+ * Reset current menu selection to default item.
+ */
+ EP.resetCurrentSizeMenuToDefault = function () {
+ this.currentSizeMenu.chooseItem( this.currentDefaultItem );
+ // Force select logic to update the selected item bar, otherwise we end up
+ // with the wrong label. This is implementation dependent and maybe it should
+ // be done via a to flag to OO.ui.SelectWidget.prototype.chooseItem()?
+ this.currentSizeMenu.emit( 'select', this.currentDefaultItem );
+ };
+
+ /**
+ * Changes the size, takes different actions based on which sort of
+ * embed is currently chosen.
+ *
+ * @param {number} width New width to set
+ * @param {number} height New height to set
+ */
+ EP.changeSize = function ( width, height ) {
+ var currentItem = this.embedSwitch.findSelectedItem();
+
+ if ( currentItem === null ) {
+ return;
+ }
+
+ switch ( currentItem.getData() ) {
+ case 'html':
+ this.updateEmbedHtml( {}, width, height );
+ break;
+ case 'wikitext':
+ this.updateEmbedWikitext( width );
+ break;
+ }
+
+ this.select();
+ };
+
+ /**
+ * Sets the HTML embed text.
+ *
+ * Assumes that the set() method has already been called to update this.embedFileInfo
+ *
+ * @param {mw.mmv.model.Thumbnail} thumbnail (can be just an empty object)
+ * @param {number} width New width to set
+ * @param {number} height New height to set
+ */
+ EP.updateEmbedHtml = function ( thumbnail, width, height ) {
+ var src;
+
+ if ( !this.embedFileInfo ) {
+ return;
+ }
+
+ src = thumbnail.url || this.embedFileInfo.imageInfo.url;
+
+ // If the image dimension requested are "large", use the current image url
+ if ( width > EP.LARGE_IMAGE_WIDTH_THRESHOLD || height > EP.LARGE_IMAGE_HEIGHT_THRESHOLD ) {
+ src = this.embedFileInfo.imageInfo.url;
+ }
+
+ this.embedTextHtml.setValue(
+ this.formatter.getThumbnailHtml( this.embedFileInfo, src, width, height ) );
+ };
+
+ /**
+ * Updates the wikitext embed text with a new value for width.
+ *
+ * Assumes that the set method has already been called.
+ *
+ * @param {number} width
+ */
+ EP.updateEmbedWikitext = function ( width ) {
+ if ( !this.embedFileInfo ) {
+ return;
+ }
+
+ this.embedTextWikitext.setValue(
+ this.formatter.getThumbnailWikitextFromEmbedFileInfo( this.embedFileInfo, width )
+ );
+ };
+
+ /**
+ * Shows the pane.
+ */
+ EP.show = function () {
+ mw.mmv.ui.reuse.Tab.prototype.show.call( this );
+ this.select();
+ };
+
+ /**
+ * Gets size options for html and wikitext snippets.
+ *
+ * @param {number} width
+ * @param {number} height
+ * @return {Object}
+ * @return {Object} return.html Collection of possible image sizes for html snippets
+ * @return {Object} return.wikitext Collection of possible image sizes for wikitext snippets
+ */
+ EP.getSizeOptions = function ( width, height ) {
+ var sizes = {};
+
+ sizes.html = this.utils.getPossibleImageSizesForHtml( width, height );
+ sizes.wikitext = this.getPossibleImageSizesForWikitext( width, height );
+
+ return sizes;
+ };
+
+ /**
+ * Sets the data on the element.
+ *
+ * @param {mw.mmv.model.Image} image
+ * @param {mw.mmv.model.Repo} repo
+ * @param {string} caption
+ * @param {string} alt
+ */
+ EP.set = function ( image, repo, caption, alt ) {
+ var embed = this,
+ htmlSizeSwitch = this.embedSizeSwitchHtml.getMenu(),
+ htmlSizeOptions = htmlSizeSwitch.getItems(),
+ wikitextSizeSwitch = this.embedSizeSwitchWikitext.getMenu(),
+ wikitextSizeOptions = wikitextSizeSwitch.getItems(),
+ sizes = this.getSizeOptions( image.width, image.height );
+
+ this.embedFileInfo = new mw.mmv.model.EmbedFileInfo( image, repo, caption, alt );
+
+ this.utils.updateMenuOptions( sizes.html, htmlSizeOptions );
+ this.utils.updateMenuOptions( sizes.wikitext, wikitextSizeOptions );
+
+ // Reset defaults
+ this.isSizeMenuDefaultReset = false;
+ this.resetCurrentSizeMenuToDefault();
+
+ this.utils.getThumbnailUrlPromise( this.LARGE_IMAGE_WIDTH_THRESHOLD )
+ .done( function ( thumbnail ) {
+ embed.updateEmbedHtml( thumbnail );
+ embed.select();
+ } );
+ };
+
+ /**
+ * @inheritdoc
+ */
+ EP.empty = function () {
+ this.embedTextHtml.setValue( '' );
+ this.embedTextWikitext.setValue( '' );
+
+ this.embedSizeSwitchHtml.getMenu().toggle( false );
+ this.embedSizeSwitchWikitext.getMenu().toggle( false );
+ };
+
+ /**
+ * Selects the text in the current textbox by triggering a focus event.
+ */
+ EP.select = function () {
+ this.$currentMainEmbedText.focus();
+ };
+
+ /**
+ * Calculates possible image sizes for wikitext snippets. It returns up to
+ * three possible snippet frame sizes (small, medium, large).
+ *
+ * @param {number} width
+ * @param {number} height
+ * @return {Object}
+ * @return {Object} return.small
+ * @return {Object} return.medium
+ * @return {Object} return.large
+ */
+ EP.getPossibleImageSizesForWikitext = function ( width, height ) {
+ var i, bucketName,
+ bucketWidth,
+ buckets = {
+ small: 300,
+ medium: 400,
+ large: 500
+ },
+ sizes = {},
+ bucketNames = Object.keys( buckets ),
+ widthToHeight = height / width;
+
+ for ( i = 0; i < bucketNames.length; i++ ) {
+ bucketName = bucketNames[ i ];
+ bucketWidth = buckets[ bucketName ];
+
+ if ( width > bucketWidth ) {
+ sizes[ bucketName ] = {
+ width: bucketWidth,
+ height: Math.round( bucketWidth * widthToHeight )
+ };
+ }
+ }
+
+ sizes.default = { width: null, height: null };
+
+ return sizes;
+ };
+
+ mw.mmv.ui.reuse.Embed = Embed;
+}( mediaWiki, jQuery, OO ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.reuse.embed.less b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.reuse.embed.less
new file mode 100644
index 00000000..52333e87
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.reuse.embed.less
@@ -0,0 +1,77 @@
+@switch-color: #f8f9fa;
+@active-switch-color: #54595d;
+@embed-dimensions-color: #72777d;
+@textarea-text-color: #222;
+
+.mw-mmv-reuse-pane {
+ .mw-mmv-embed-text-html,
+ .mw-mmv-embed-text-wikitext {
+ display: none;
+ width: auto;
+
+ &.active {
+ display: block;
+ }
+
+ // The selector has to be that deep and include [readonly] to override OOUI's definition
+ textarea[ readonly ] {
+ color: @textarea-text-color;
+ text-shadow: none;
+ word-wrap: break-word;
+ }
+ }
+}
+
+.mw-mmv-reuse-dialog .mw-mmv-embed-pane {
+ padding: 20px 27px;
+
+ .mw-mmv-embed-explanation {
+ margin: 10px 0 20px 0;
+ padding: 0;
+ }
+
+ p,
+ textarea {
+ margin: 0;
+ }
+
+ .mw-mmv-dialog-copy {
+ float: right;
+ width: 1.5em;
+ height: 1.5em;
+ margin: 10px 0.75em 20px 0.75em;
+ }
+}
+
+.mw-mmv-embed-switch {
+ div {
+ padding: 1px 3px;
+ background-color: @switch-color;
+
+ &.active {
+ background-color: @active-switch-color;
+ }
+ }
+}
+
+.mw-mmv-embed-size {
+ display: none;
+ margin: 10px 0 0 0;
+
+ &.active {
+ display: block;
+ }
+
+ .oo-ui-widget-disabled {
+ display: none;
+ }
+}
+
+.mw-mmv-embed-dimensions {
+ color: @embed-dimensions-color;
+ font-size: small;
+
+ .oo-ui-optionWidget-selected & {
+ color: lighten( @embed-dimensions-color, 20% ); // selected option has dark background
+ }
+}
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.reuse.share.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.reuse.share.js
new file mode 100644
index 00000000..98b8722a
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.reuse.share.js
@@ -0,0 +1,166 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $, oo ) {
+ // Shortcut for prototype later
+ var SP;
+
+ /**
+ * Represents the file reuse dialog and link to open it.
+ *
+ * @class mw.mmv.ui.reuse.Share
+ * @extends mw.mmv.ui.reuse.Tab
+ * @param {jQuery} $container
+ */
+ function Share( $container ) {
+ mw.mmv.ui.reuse.Tab.call( this, $container );
+
+ /**
+ * @property {mw.mmv.routing.Router} router -
+ */
+ this.router = new mw.mmv.routing.Router();
+
+ this.init();
+ }
+ oo.inheritClass( Share, mw.mmv.ui.reuse.Tab );
+ SP = Share.prototype;
+
+ SP.init = function () {
+ var pane = this;
+
+ this.$pane.addClass( 'mw-mmv-share-pane' )
+ .appendTo( this.$container );
+
+ this.pageInput = new oo.ui.TextInputWidget( {
+ classes: [ 'mw-mmv-share-page' ],
+ readOnly: true
+ } );
+
+ this.pageInput.$element.find( 'input' )
+ .prop( 'placeholder', mw.message( 'multimediaviewer-reuse-loading-placeholder' ).text() );
+
+ this.pageInput.$input.on( 'copy', function () {
+ mw.mmv.actionLogger.log( 'share-link-copied' );
+ } );
+
+ this.$pageLink = $( '<a>' )
+ .addClass( 'mw-mmv-share-page-link' )
+ .prop( 'alt', mw.message( 'multimediaviewer-link-to-page' ).text() )
+ .prop( 'target', '_blank' )
+ .html( '&nbsp;' )
+ .appendTo( this.$pane )
+ .click( function () {
+ mw.mmv.actionLogger.log( 'share-page' );
+ } );
+
+ this.$copyButton = $( '<button>' )
+ .addClass( 'mw-mmv-button mw-mmv-dialog-copy' )
+ .click( function () {
+ // Select the text, and then try to copy the text.
+ // If the copy fails or is not supported, continue as if nothing had happened.
+ pane.pageInput.$input.select();
+ try {
+ if ( document.queryCommandSupported &&
+ document.queryCommandSupported( 'copy' ) ) {
+ document.execCommand( 'copy' );
+ }
+ } catch ( e ) {
+ // queryCommandSupported in Firefox pre-41 can throw errors when used with
+ // clipboard commands. We catch and ignore these and other copy-command-related
+ // errors here.
+ }
+ } )
+ .prop( 'title', mw.msg( 'multimediaviewer-reuse-copy-share' ) )
+ .text( mw.msg( 'multimediaviewer-reuse-copy-share' ) )
+ .tipsy( {
+ delayIn: mw.config.get( 'wgMultimediaViewer' ).tooltipDelay,
+ gravity: this.correctEW( 'se' )
+ } )
+ .appendTo( this.$pane );
+
+ this.pageInput.$element.appendTo( this.$pane );
+
+ this.$explanation = $( '<div>' )
+ .addClass( 'mw-mmv-shareembed-explanation' )
+ .text( mw.message( 'multimediaviewer-share-explanation' ).text() )
+ .appendTo( this.$pane );
+
+ this.$pane.appendTo( this.$container );
+ };
+
+ /**
+ * Shows the pane.
+ */
+ SP.show = function () {
+ mw.mmv.ui.reuse.Tab.prototype.show.call( this );
+ this.select();
+ };
+
+ /**
+ * @inheritdoc
+ * @param {mw.mmv.model.Image} image
+ */
+ SP.set = function ( image ) {
+ var route = new mw.mmv.routing.ThumbnailRoute( image.title ),
+ url = this.router.createHashedUrl( route, image.descriptionUrl );
+
+ this.pageInput.setValue( url );
+
+ this.select();
+
+ this.$pageLink.prop( 'href', url );
+ };
+
+ /**
+ * @inheritdoc
+ */
+ SP.empty = function () {
+ this.pageInput.setValue( '' );
+ this.$pageLink.prop( 'href', null );
+ };
+
+ /**
+ * @inheritdoc
+ */
+ SP.attach = function () {
+ var $input = this.pageInput.$element.find( 'input' );
+
+ $input.on( 'focus', this.selectAllOnEvent );
+ // Disable partial text selection inside the textbox
+ $input.on( 'mousedown click', this.onlyFocus );
+ };
+
+ /**
+ * @inheritdoc
+ */
+ SP.unattach = function () {
+ var $input = this.pageInput.$element.find( 'input' );
+
+ mw.mmv.ui.reuse.Tab.prototype.unattach.call( this );
+
+ $input.off( 'focus mousedown click' );
+ };
+
+ /**
+ * Selects the text in the readonly textbox by triggering a focus event.
+ */
+ SP.select = function () {
+ this.pageInput.$element.focus();
+ };
+
+ mw.mmv.ui.reuse.Share = Share;
+}( mediaWiki, jQuery, OO ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.reuse.share.less b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.reuse.share.less
new file mode 100644
index 00000000..fac60a81
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.reuse.share.less
@@ -0,0 +1,49 @@
+@input-text-color: #222;
+
+.mw-mmv-share-pane {
+ .mw-mmv-share-page {
+ // override OOUI fixed width
+ width: auto;
+ // make sure this is a new block formatting context so that the float shrinks it instead of
+ // running into it and pushing the input down (there is probably a better way of doing this)
+ overflow: hidden;
+
+ // The selector has to be that deep and include [readonly] to override OOUI's definition
+ input[ readonly ] {
+ color: @input-text-color;
+ text-shadow: none;
+ }
+ }
+
+ .mw-mmv-dialog-copy {
+ // style rules based on .mw-mmv-share-page-link
+ float: right;
+ width: 2em;
+ height: 2em;
+ // position approximately to the middle - probably fragile but couldn't find a better way as
+ // the height of OOUI input widget has both em and px parts and not possible to calculate
+ // exactly
+ margin: 8px 0.5em;
+ }
+}
+
+.mw-mmv-share-page-link {
+ float: left;
+ width: 1.5em;
+ height: 1.5em;
+ // position approximately to the middle - probably fragile but couldn't find a better way as the
+ // height of OOUI input widget has both em and px parts and not possible to calculate exactly
+ margin: 8px 0;
+ padding: 0 5px 0 0;
+ /* @embed */
+ background-image: url( img/link.svg );
+ background-size: contain;
+ background-position: left center;
+ background-repeat: no-repeat;
+
+ &:hover {
+ /* @embed */
+ background-image: url( img/link-hover.svg );
+ text-decoration: none;
+ }
+}
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.reuse.shareembed.less b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.reuse.shareembed.less
new file mode 100644
index 00000000..d5c68919
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.reuse.shareembed.less
@@ -0,0 +1,8 @@
+@explanation-color: #72777d;
+
+.mw-mmv-shareembed-explanation {
+ color: @explanation-color;
+ font-size: 0.9em;
+ margin-left: 1.5em;
+ padding: 5px 8px;
+}
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.reuse.tab.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.reuse.tab.js
new file mode 100644
index 00000000..74735c04
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.reuse.tab.js
@@ -0,0 +1,57 @@
+/*
+ * This file is part of the MediaWiki extension MediaViewer.
+ *
+ * MediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $, oo ) {
+ var TP;
+
+ /**
+ * A tab in a mw.mmv.ui.Pane component
+ *
+ * @class mw.mmv.ui.reuse.Tab
+ * @extends mw.mmv.ui.Element
+ * @param {jQuery} $container
+ * @constructor
+ */
+ function Tab( $container ) {
+ Tab.super.call( this, $container );
+
+ /**
+ * Container for the tab.
+ * @property {jQuery}
+ */
+ this.$pane = $( '<div>' ).addClass( 'mw-mmv-reuse-pane' );
+
+ }
+ oo.inheritClass( Tab, mw.mmv.ui.Element );
+ TP = Tab.prototype;
+
+ /**
+ * Shows the pane.
+ */
+ TP.show = function () {
+ this.$pane.addClass( 'active' );
+ };
+
+ /**
+ * Hides the pane.
+ */
+ TP.hide = function () {
+ this.$pane.removeClass( 'active' );
+ };
+
+ mw.mmv.ui.reuse.Tab = Tab;
+}( mediaWiki, jQuery, OO ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.stripeButtons.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.stripeButtons.js
new file mode 100644
index 00000000..75497df9
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.stripeButtons.js
@@ -0,0 +1,136 @@
+/*
+ * This file is part of the MediaWiki extension MediaViewer.
+ *
+ * MediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $, oo ) {
+ var SBP;
+
+ /**
+ * Class for buttons which are placed on the metadata stripe (the always visible part of the
+ * metadata panel).
+ *
+ * @class mw.mmv.ui.StripeButtons
+ * @extends mw.mmv.ui.Element
+ * @constructor
+ * @param {jQuery} $container the title block (.mw-mmv-title-contain) which wraps the buttons and all
+ * other title elements
+ */
+ function StripeButtons( $container ) {
+ mw.mmv.ui.Element.call( this, $container );
+
+ this.$buttonContainer = $( '<div>' )
+ .addClass( 'mw-mmv-stripe-button-container' )
+ .appendTo( $container );
+
+ /**
+ * This holds the actual buttons.
+ * @property {Object.<string, jQuery>}
+ */
+ this.buttons = {};
+
+ this.initDescriptionPageButton();
+ }
+ oo.inheritClass( StripeButtons, mw.mmv.ui.Element );
+ SBP = StripeButtons.prototype;
+
+ /**
+ * Creates a new button on the metadata stripe.
+ *
+ * @protected
+ * @param {string} cssClass CSS class name for the button
+ * @return {jQuery} Button
+ */
+ SBP.createButton = function ( cssClass ) {
+ var $button;
+
+ $button = $( '<a>' )
+ .addClass( 'mw-mmv-stripe-button empty ' + cssClass )
+ // elements are right-floated so we use prepend instead of append to keep the order
+ .prependTo( this.$buttonContainer )
+ .attr( 'tabindex', '0' );
+
+ return $button;
+ };
+
+ /**
+ * Creates a button linking to the file description page.
+ *
+ * @protected
+ */
+ SBP.initDescriptionPageButton = function () {
+ this.buttons.$descriptionPage = this.createButton(
+ 'empty mw-mmv-description-page-button mw-ui-big mw-ui-button mw-ui-progressive'
+ ).click( function () {
+ mw.mmv.actionLogger.log( 'file-description-page-abovefold' );
+ } );
+ };
+
+ /**
+ * Runs code for each button, similarly to $.each.
+ *
+ * @protected
+ * @param {function(jQuery, string)} callback a function that will be called with each button
+ */
+ SBP.eachButton = function ( callback ) {
+ var buttonName;
+ for ( buttonName in this.buttons ) {
+ callback( this.buttons[ buttonName ], buttonName );
+ }
+ };
+
+ /**
+ * @inheritdoc
+ * @param {mw.mmv.model.Image} imageInfo
+ * @param {mw.mmv.model.Repo} repoInfo
+ */
+ SBP.set = function ( imageInfo, repoInfo ) {
+ this.eachButton( function ( $button ) {
+ $button.removeClass( 'empty' );
+ } );
+
+ this.setDescriptionPageButton( imageInfo, repoInfo );
+ };
+
+ /**
+ * Updates the button linking to the file page.
+ *
+ * @protected
+ * @param {mw.mmv.model.Image} imageInfo
+ * @param {mw.mmv.model.Repo} repoInfo
+ */
+ SBP.setDescriptionPageButton = function ( imageInfo, repoInfo ) {
+ var $button = this.buttons.$descriptionPage;
+
+ $button.text( mw.message( 'multimediaviewer-repository-local' ).text() )
+ .attr( 'href', imageInfo.descriptionUrl );
+
+ $button.toggleClass( 'mw-mmv-repo-button-commons', repoInfo.isCommons() );
+ };
+
+ /**
+ * @inheritdoc
+ */
+ SBP.empty = function () {
+ this.eachButton( function ( $button ) {
+ $button.addClass( 'empty' );
+ } );
+
+ this.buttons.$descriptionPage.attr( { href: null, title: null, 'original-title': null } )
+ .removeClass( 'mw-mmv-repo-button-commons' );
+ };
+
+ mw.mmv.ui.StripeButtons = StripeButtons;
+}( mediaWiki, jQuery, OO ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.stripeButtons.less b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.stripeButtons.less
new file mode 100644
index 00000000..e9d4f66c
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.stripeButtons.less
@@ -0,0 +1,68 @@
+@import '../mmv.globals';
+@import '../mmv.mixins';
+
+@button-vertical-padding: 20px;
+@button-height: 25px;
+@repo-button-width: 50px;
+
+// The buttons need to be positioned to the bottom right corner of .mw-mmv-title-contain, and
+// other text in that element must not overlap them. Their width is not known (depends on the
+// translation) so we float them to the right and calculate the top margin required to make them
+// full height.
+.mw-mmv-stripe-button {
+ @button-text-color: #888;
+
+ float: right;
+ margin-top: @metadatabar-above-fold-inner-height - ( @button-height + 2 * @button-vertical-padding );
+
+ .unselectable;
+ font-size: 1.25em;
+ color: @button-text-color;
+ cursor: pointer;
+ transition: opacity 0.25s;
+
+ &.empty {
+ display: none;
+ }
+
+ &:before {
+ display: inline-block;
+ width: 1em;
+ height: 1em;
+
+ // fix odd position caused by bottom of icon and bottom of SVG not being aligned
+ position: relative;
+ top: 0.1em;
+
+ background-size: 100% 100%;
+ content: ' ';
+ vertical-align: baseline;
+
+ /* @embed */
+ background-image: url( img/page.svg );
+
+ margin-right: 0.7em;
+ }
+ &.has-label:before {
+ margin-right: 0.25em;
+ }
+
+ &.mw-mmv-description-page-button,
+ &.mw-mmv-description-page-button:active,
+ &.mw-mmv-description-page-button:visited {
+ border-radius: @border-radius;
+ color: #fff;
+ padding: 7px 16px;
+ margin: 7px 10px;
+ }
+
+ &.mw-mmv-description-page-button.mw-mmv-repo-button-commons:before {
+ /* @embed */
+ background-image: url( img/commons_white.svg );
+ width: 1.3em;
+ height: 1.3em;
+ position: relative;
+ top: 0.15em;
+ margin: -0.3em 0.4em 0 0;
+ }
+}
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.tipsyDialog.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.tipsyDialog.js
new file mode 100644
index 00000000..8202d5e7
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.tipsyDialog.js
@@ -0,0 +1,202 @@
+/*
+ * This file is part of the MediaWiki extension MediaViewer.
+ *
+ * MediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+( function ( mw, $, oo ) {
+ var TDP;
+
+ /**
+ * A simple popup dialog that can be opened and closed and can contain some HTML.
+ * Due to the way tipsy works, there can be only one TipsyDialog and/or tipsy tooltip on the same element.
+ *
+ * @class mw.mmv.ui.TipsyDialog
+ * @extends mw.mmv.ui.Element
+ * @constructor
+ * @param {jQuery} $anchor the element to which the popup is anchored.
+ * @param {Object} options takes any tipsy option - see
+ * https://github.com/jaz303/tipsy/blob/master/docs/src/index.html.erb#L298
+ */
+ function TipsyDialog( $anchor, options ) {
+ mw.mmv.ui.Element.call( this, null ); // tipsy does the element construction so we don't need a container
+
+ /** @property {jQuery} $anchor - */
+ this.$anchor = $anchor;
+
+ /** @property {Object} options - */
+ this.options = $.extend( {}, this.defaultOptions, options );
+
+ /** @property {boolean} dirty Track whether tipsy settings changed and need to be reinitialized. */
+ this.dirty = false;
+
+ /** @property {string|null} contentTitle Title of the dialog (optional) */
+ this.contentTitle = null;
+
+ /** @property {string|null} contentBody Contents of the dialog */
+ this.contentBody = null;
+
+ /** @property {Function} closeProxy Proxied close function to be used as an event handler, so it can be
+ * identified for removal. */
+ this.closeProxy = $.proxy( this.maybeCloseOnClick, this );
+ }
+
+ oo.inheritClass( TipsyDialog, mw.mmv.ui.Element );
+ TDP = TipsyDialog.prototype;
+
+ /**
+ * @property {Object} defaultOptions Tipsy defaults - see
+ * https://github.com/jaz303/tipsy/blob/master/docs/src/index.html.erb#L298
+ */
+ TDP.defaultOptions = {
+ // tipsy options
+ trigger: 'manual',
+ html: true,
+ fade: false,
+ offset: 0,
+ gravity: 'sw'
+ };
+
+ /**
+ * @property {number} extraOffset offset adjustment to correct for the larger margins and tip size
+ * compared to the standard tipsy style
+ */
+ TDP.extraOffset = 10;
+
+ /**
+ * @private
+ * @return {boolean}
+ */
+ TDP.isInitialized = function () {
+ return !!this.$anchor.tipsy( true );
+ };
+
+ /**
+ * Returns the preprocessed version of an options object:
+ * - directions are flipped on RTL documents
+ * - standard classnames are applied
+ * - HTML content is generated
+ * The original object is not changed.
+ *
+ * @private
+ * @param {Object} originalOptions
+ * @return {Object} Preprocessed options
+ */
+ TDP.getPreprocessedOptions = function ( originalOptions ) {
+ var options = $.extend( {}, originalOptions );
+
+ if ( options.className ) {
+ options.className += ' mw-mmv-tipsy-dialog';
+ } else {
+ options.className = ' mw-mmv-tipsy-dialog';
+ }
+ options.gravity = this.correctEW( options.gravity );
+ options.offset += this.extraOffset;
+ options.fallback = this.generateContent( this.contentTitle, this.contentBody );
+
+ return options;
+ };
+
+ /**
+ * @private
+ */
+ TDP.init = function () {
+ var options;
+
+ if ( !this.isInitialized() || this.dirty ) {
+ options = this.getPreprocessedOptions( this.options );
+ this.$anchor.tipsy( options );
+
+ // add click handler to close the popup when clicking on X or outside
+ // off is to make sure we won't end up with more then one - init() can be called multiple times
+ this.$anchor.find( '.mw-mmv-tipsy-dialog-disable' ).add( document )
+ .off( 'click.mmv-tipsy-dialog', this.closeProxy )
+ .on( 'click.mmv-tipsy-dialog', this.closeProxy );
+
+ this.dirty = false;
+ }
+ };
+
+ /**
+ * Open the dialog
+ */
+ TDP.open = function () {
+ this.init();
+ this.$anchor.tipsy( 'enable' ).tipsy( 'show' );
+ };
+
+ /**
+ * Close the dialog
+ */
+ TDP.close = function () {
+ if ( this.isInitialized() ) {
+ this.$anchor.tipsy( 'hide' ).tipsy( 'disable' );
+ }
+ };
+
+ /**
+ * Return the main popup element.
+ *
+ * @return {jQuery|null}
+ */
+ TDP.getPopup = function () {
+ var tipsyData = this.$anchor.tipsy( true );
+
+ return tipsyData ? tipsyData.$tip : null;
+ };
+
+ /**
+ * Set dialog contents
+ *
+ * @param {string|null} title title of the dialog (plain text; escaping will be handled by TipsyDialog)
+ * @param {string|null} body content of the dialog (HTML; no escaping)
+ */
+ TDP.setContent = function ( title, body ) {
+ this.contentTitle = title;
+ this.contentBody = body;
+ this.dirty = true;
+ };
+
+ /**
+ * @private
+ * @param {string} [title]
+ * @param {string} [body]
+ * @return {string}
+ */
+ TDP.generateContent = function ( title, body ) {
+ body = body || '';
+ if ( title ) {
+ body = '<div class="mw-mmv-tipsy-dialog-title">' + mw.html.escape( title ) + '</div>' + body;
+ }
+ return '<div class="mw-mmv-tipsy-dialog-disable"></div>' + body;
+ };
+
+ /**
+ * Click handler to be set on the document.
+ *
+ * @private
+ * @param {jQuery.Event} event
+ */
+ TDP.maybeCloseOnClick = function ( event ) {
+ var $clickTarget = $( event.target );
+
+ if (
+ $clickTarget.closest( this.getPopup() ).length === 0 || // click was outside the dialog
+ $clickTarget.closest( '.mw-mmv-tipsy-dialog-disable' ).length > 0 // click was on the close icon
+ ) {
+ this.close();
+ }
+ };
+
+ mw.mmv.ui.TipsyDialog = TipsyDialog;
+}( mediaWiki, jQuery, OO ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.tipsyDialog.less b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.tipsyDialog.less
new file mode 100644
index 00000000..bd21a13b
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.tipsyDialog.less
@@ -0,0 +1,56 @@
+@import '../mmv.globals';
+@import '../mmv.mixins';
+
+.mw-mmv-tipsy-dialog {
+ @arrow-height: 8px;
+ @background-color: #eaecf0;
+ @border-color: #a2a9b1;
+ @padding: 20px; // must be larger than @arrow-height / sqrt(2), otherwise the arrow overlaps the text
+
+ padding-bottom: @arrow-height; // the actual arrow height is @arrow-height / sqrt(2)
+
+ .tipsy-inner {
+ max-width: 400px;
+ background-color: @background-color;
+ border: 1px solid @border-color;
+ border-radius: @border-radius;
+ padding: @padding;
+ color: #54595d;
+
+ .mw-mmv-tipsy-dialog-title {
+ margin-bottom: 1em;
+ color: #222;
+ font-size: 130%;
+ }
+ }
+
+ .tipsy-arrow {
+ width: 2 * @arrow-height;
+ height: 2 * @arrow-height;
+ position: absolute;
+ bottom: 0;
+ left: 10px + @arrow-height;
+ background: @background-color;
+ border: 1px solid @border-color;
+ border-width: 0 0 1px 1px;
+ .rotate( -45deg );
+ }
+
+ .mw-mmv-tipsy-dialog-disable {
+ @distanceFromBorder: 8px;
+ @topRightMargin: @distanceFromBorder - @padding;
+ width: 15px;
+ height: 15px;
+ float: right;
+ margin: @topRightMargin @topRightMargin @padding @padding;
+ cursor: pointer;
+ /* @embed */
+ background-image: url( img/x_gray.svg );
+ background-repeat: no-repeat;
+ opacity: 0.75;
+
+ &:hover {
+ opacity: 1;
+ }
+ }
+}
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.truncatableTextField.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.truncatableTextField.js
new file mode 100644
index 00000000..c14b4a98
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.truncatableTextField.js
@@ -0,0 +1,240 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $, oo ) {
+ var TTFP;
+
+ /**
+ * Represents any text field that might need to be truncated to be readable. Text will be adjusted to
+ * fit into its container.
+ *
+ * More specifically, TruncatableTextField should be invoked with a fixed-height container as the first
+ * parameter and a flexible-width content (which gets its size from the text inside it) as the second
+ * one. The container gets overflow: hidden, and the content is placed inside it; if the content
+ * overflows the container, TruncatableTextField will cycle through a set of styles and apply to the
+ * container the first one that makes the content not overflow anymore. If none of the styles do that,
+ * the last one is applied anyway.
+ *
+ * The list of styles can be customized; by default, they set progressively smaller font size, and the
+ * last one adds an ellipsis to the end. (An ellipsis element is automatically appended to the end of
+ * the container to help with this, but it is hidden unless made visible by one of the styles.)
+ *
+ * grow() and shrink() can be used to show full text (by making the container flexible-height) and hiding
+ * them again; TruncatableTextField will not call them automatically (the caller class should e.g. set up
+ * a click handler on the ellipsis).
+ *
+ * repaint() should be called after layout changes to keep the truncation accurate.
+ *
+ * @class mw.mmv.ui.TruncatableTextField
+ * @extends mw.mmv.ui.Element
+ * @constructor
+ * @param {jQuery} $container The container for the element.
+ * @param {jQuery} $element The element where we should put the text.
+ * @param {Object} [options]
+ * @param {string[]} [options.styles] a list of styles to try if the text does not fit into the container.
+ * Will stop at the first which makes the text fit; the last one will be used even if it does not make
+ * the text fit.
+ */
+ function TruncatableTextField( $container, $element, options ) {
+ mw.mmv.ui.Element.call( this, $container );
+
+ /** @property {jQuery} $element The DOM element that holds text for this element. */
+ this.$element = $element;
+
+ /** @property {Object} options - */
+ this.options = $.extend( {}, this.defaultOptions, options );
+
+ /** @property {boolean} expanded true if the text is long enough to be truncated but the full text is shown */
+ this.expanded = false;
+
+ /** @property {jQuery} ellipsis the element which marks that the text was truncated */
+ this.$ellipsis = null;
+
+ /** @property {string} normalTitle title attribute to show when the text is not truncated */
+ this.normalTitle = null;
+
+ /** @property {string} truncatedTitle title attribute to show when the text is not truncated */
+ this.truncatedTitle = null;
+
+ /** @property {mw.mmv.HtmlUtils} htmlUtils Our HTML utility instance. */
+ this.htmlUtils = new mw.mmv.HtmlUtils();
+
+ this.init();
+ }
+
+ oo.inheritClass( TruncatableTextField, mw.mmv.ui.Element );
+
+ TTFP = TruncatableTextField.prototype;
+
+ /**
+ * Default options
+ * @property {Object} defaultOptions
+ */
+ TTFP.defaultOptions = {
+ styles: [ 'mw-mmv-ttf-small', 'mw-mmv-ttf-smaller', 'mw-mmv-ttf-smallest' ]
+ };
+
+ /**
+ * Initializes the DOM.
+ *
+ * @private
+ */
+ TTFP.init = function () {
+ this.$ellipsis = $( '<span>' )
+ .text( '…' )
+ .hide()
+ .addClass( 'mw-mmv-ttf-ellipsis' );
+
+ this.$container
+ .addClass( 'mw-mmv-ttf-container empty' )
+ .append( this.$element, this.$ellipsis );
+ };
+
+ TTFP.attach = function () {
+ $( window ).on( 'resize.mmv-ttf', $.debounce( 100, $.proxy( this.repaint, this ) ) );
+ };
+
+ TTFP.unattach = function () {
+ $( window ).off( 'resize.mmv-ttf' );
+ };
+
+ /**
+ * Sets the string for the element.
+ *
+ * @param {string} value Warning - unsafe HTML is allowed here.
+ */
+ TTFP.set = function ( value ) {
+ this.$element.empty().append( this.htmlUtils.htmlToTextWithTags( value ) );
+ this.changeStyle();
+ this.$container.toggleClass( 'empty', !value );
+ this.$ellipsis.hide();
+ this.shrink();
+ };
+
+ TTFP.empty = function () {
+ this.$element.empty();
+ this.$container
+ .removeClass( this.options.styles.join( ' ' ) )
+ .removeClass( 'mw-mmv-ttf-untruncated mw-mmv-ttf-truncated' )
+ .addClass( 'empty' );
+ this.$ellipsis.hide();
+ this.setTitle( '', '' );
+ this.expanded = false;
+ };
+
+ /**
+ * Recalculate truncation after layout changes (such as resize)
+ */
+ TTFP.repaint = function () {
+ this.changeStyle();
+ this.$ellipsis.hide();
+ this.shrink();
+ };
+
+ /**
+ * Allows setting different titles for fully visible and for truncated text.
+ *
+ * @param {string} normal
+ * @param {string} truncated
+ */
+ TTFP.setTitle = function ( normal, truncated ) {
+ this.normalTitle = normal;
+ this.truncatedTitle = truncated;
+ this.updateTitle();
+ };
+
+ /**
+ * Selects the right title to use (for full or for truncated version). The title can be set with setTitle().
+ */
+ TTFP.updateTitle = function () {
+ var $elementsWithTitle = this.$element.add( this.$ellipsis );
+ $elementsWithTitle.attr( 'original-title', this.isTruncated() ? this.truncatedTitle : this.normalTitle );
+ };
+
+ /**
+ * Returns true if the text is long enough that it needs to be truncated.
+ *
+ * @return {boolean}
+ */
+ TTFP.isTruncatable = function () {
+ // height calculation logic does not work for expanded state since the container expands
+ // to envelop the element, but we never go into expanded state for non-truncatable elements anyway
+ return this.$container.height() < this.$element.height() || this.expanded;
+ };
+
+ /**
+ * Returns true if the text is truncated at the moment.
+ *
+ * @return {boolean}
+ */
+ TTFP.isTruncated = function () {
+ return this.isTruncatable() && !this.expanded;
+ };
+
+ /**
+ * Makes the container fixed-width, clipping the text.
+ * This will only add a .mw-mmv-ttf-truncated class; it's the caller's responsibility to define the fixed
+ * height for that class.
+ */
+ TTFP.shrink = function () {
+ if ( this.isTruncatable() ) {
+ this.expanded = false;
+ this.$container.addClass( 'mw-mmv-ttf-truncated' ).removeClass( 'mw-mmv-ttf-untruncated' );
+ this.$ellipsis.show();
+ this.updateTitle();
+ }
+ };
+
+ /**
+ * Makes the container flexible-width, thereby restoring the full text.
+ */
+ TTFP.grow = function () {
+ if ( this.isTruncatable() ) {
+ this.expanded = true;
+ this.$container.removeClass( 'mw-mmv-ttf-truncated' ).addClass( 'mw-mmv-ttf-untruncated' );
+ this.$ellipsis.hide();
+ this.updateTitle();
+ }
+ };
+
+ /**
+ * Changes the element style if a certain length is reached.
+ */
+ TTFP.changeStyle = function () {
+ var oldClass,
+ newClass = 'mw-mmv-ttf-normal',
+ field = this;
+
+ this.$container
+ .removeClass( this.options.styles.join( ' ' ) )
+ .removeClass( 'mw-mmv-ttf-untruncated mw-mmv-ttf-truncated' )
+ .addClass( newClass );
+ this.expanded = false;
+
+ $.each( this.options.styles, function ( k, v ) {
+ if ( !field.isTruncatable() ) {
+ return false;
+ }
+
+ oldClass = newClass;
+ newClass = v;
+ field.$container.removeClass( oldClass ).addClass( newClass );
+ } );
+ };
+
+ mw.mmv.ui.TruncatableTextField = TruncatableTextField;
+}( mediaWiki, jQuery, OO ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.truncatableTextField.less b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.truncatableTextField.less
new file mode 100644
index 00000000..f3a5f77e
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.truncatableTextField.less
@@ -0,0 +1,74 @@
+@import 'mediawiki.mixins';
+@import '../mmv.globals';
+@import '../mmv.mixins';
+
+.mw-mmv-ttf-container {
+ position: relative;
+ overflow: hidden; // need to create a block formatting context, otherwise adjacent floats would be problematic
+
+ &.mw-mmv-ttf-small {
+ font-size: 90%;
+ }
+
+ &.mw-mmv-ttf-smaller {
+ font-size: 80%;
+ }
+
+ &.mw-mmv-ttf-smallest {
+ font-size: 65%;
+ }
+
+ // used on containers which have been shortened and can be expanded
+ &.mw-mmv-ttf-truncated {
+ cursor: pointer;
+ }
+
+ // used on containers which have been expanded
+ &.mw-mmv-ttf-untruncated {
+ height: auto;
+ }
+
+ .mw-mmv-ttf-ellipsis {
+ display: block;
+ position: absolute;
+ right: 0;
+ bottom: 0;
+ height: 1em; // make it scale with the font
+ width: 1.5em; // SVG image has ~1.5 aspect ratio
+ text-indent: -9999px;
+ border-radius: @border-radius;
+ background-size: contain;
+ background-position: center center;
+ background-repeat: no-repeat;
+ /* @embed */
+ background-image: url( img/ellipsis_lightgray.svg );
+ .box-shadow( -4px 6px 0 0 #fff );
+
+ // Hovering over .mw-mmv-ttf-ellipsis-container will make the ellipsis more visible to attract
+ // attention to it. Since it has to be set on a parent element which is not under control of
+ // TruncatableTextField, setting it is the caller's responsibility.
+ .mw-mmv-ttf-ellipsis-container:hover & {
+ background-color: #eaecf0;
+ /* @embed */
+ background-image: url( img/ellipsis_gray.svg );
+ }
+ // second rule + ordering needed for specificity
+ &:hover,
+ .mw-mmv-ttf-ellipsis-container &:hover {
+ background-color: #c8ccd1;
+ /* @embed */
+ background-image: url( img/ellipsis_darkgray.svg );
+ }
+
+ &:before {
+ @fade-length: 2em;
+
+ display: block;
+ content: '';
+ height: 1.6em; // 1em would not cover high glyphs
+ width: @fade-length;
+ margin-left: -@fade-length;
+ .fade-out-horizontal();
+ }
+ }
+}
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.utils.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.utils.js
new file mode 100644
index 00000000..31af3872
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.utils.js
@@ -0,0 +1,210 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $, oo ) {
+ var RUP;
+
+ /**
+ * A helper class for reuse logic.
+ *
+ * @class mw.mmv.ui.Utils
+ * @constructor
+ */
+ function Utils() {
+ /** @property {mw.mmv.HtmlUtils} htmlUtils - */
+ this.htmlUtils = new mw.mmv.HtmlUtils();
+ }
+
+ RUP = Utils.prototype;
+
+ /**
+ * Creates pulldown menu from given options.
+ *
+ * @param {string[]} options
+ * @param {string[]} classes
+ * @param {string} def
+ * @return {OO.ui.DropdownWidget}
+ */
+ RUP.createPulldownMenu = function ( options, classes, def ) {
+ var dropdown, i, option,
+ items = [],
+ choices = {};
+
+ dropdown = new oo.ui.DropdownWidget( {
+ classes: classes
+ } );
+
+ for ( i = 0; i < options.length; i++ ) {
+ option = options[ i ];
+
+ choices[ option ] = new oo.ui.MenuOptionWidget( {
+ data: {
+ name: option,
+ height: null,
+ width: null
+ },
+ label: this.getDimensionsMessageHtml( option, 0, 0 ),
+ autoFitLabel: false
+ } );
+
+ items.push( choices[ option ] );
+ }
+
+ dropdown.getMenu()
+ .addItems( items )
+ .chooseItem( choices[ def ] );
+
+ return dropdown;
+ };
+
+ /**
+ * Gets a promise for the large thumbnail URL. This is needed because thumbnail URLs cannot
+ * be reliably guessed, even if we know the full size of the image - most of the time replacing
+ * the size in another thumbnail URL works (as long as the new size is not larger than the full
+ * size), but if the file name is very long and with the larger size the URL length would
+ * exceed a certain threshold, a different schema is used instead.
+ *
+ * @param {number} width
+ *
+ * @return {jQuery.Promise.<string>}
+ */
+ RUP.getThumbnailUrlPromise = function ( width ) {
+ return $( document ).triggerHandler( 'mmv-request-thumbnail', width ) ||
+ $.Deferred().reject();
+ };
+
+ /**
+ * Updates the menu options based on calculated sizes.
+ *
+ * @private
+ * @param {Object} sizes
+ * @param {OO.ui.MenuOptionWidget[]} options
+ */
+ RUP.updateMenuOptions = function ( sizes, options ) {
+ var i, option, data, $label;
+
+ for ( i = 0; i < options.length; i++ ) {
+ option = options[ i ];
+ data = option.getData();
+
+ if ( sizes[ data.name ] ) {
+ option.setDisabled( false );
+
+ // These values are later used when the item is selected
+ data.width = sizes[ data.name ].width;
+ data.height = sizes[ data.name ].height;
+
+ $label = $( '<span>' ).html( this.getDimensionsMessageHtml( data.name, data.width, data.height ) );
+
+ option.setLabel( $label );
+ } else {
+ option.setDisabled( true );
+ }
+ }
+ };
+
+ /**
+ * Calculates possible image sizes for html snippets. It returns up to
+ * three possible snippet frame sizes (small, medium, large) plus the
+ * original image size.
+ *
+ * @param {number} width
+ * @param {number} height
+ * @return {Object}
+ * @return {Object} return.small
+ * @return {Object} return.medium
+ * @return {Object} return.large
+ * @return {Object} return.original
+ */
+ RUP.getPossibleImageSizesForHtml = function ( width, height ) {
+ var i, bucketName,
+ currentGuess, dimensions,
+ bucketWidth, bucketHeight,
+ buckets = {
+ small: { width: 220, height: 145 },
+ medium: { width: 640, height: 480 },
+ large: { width: 1200, height: 900 }
+ },
+ sizes = {},
+ bucketNames = Object.keys( buckets ),
+ widthToHeight = height / width,
+ heightToWidth = width / height;
+
+ for ( i = 0; i < bucketNames.length; i++ ) {
+ bucketName = bucketNames[ i ];
+ dimensions = buckets[ bucketName ];
+ bucketWidth = dimensions.width;
+ bucketHeight = dimensions.height;
+
+ if ( width > bucketWidth ) {
+ // Width fits in the current bucket
+ currentGuess = bucketWidth;
+
+ if ( currentGuess * widthToHeight > bucketHeight ) {
+ // Constrain in height, resize width accordingly
+ sizes[ bucketName ] = {
+ width: Math.round( bucketHeight * heightToWidth ),
+ height: bucketHeight
+ };
+ } else {
+ sizes[ bucketName ] = {
+ width: currentGuess,
+ height: Math.round( currentGuess * widthToHeight )
+ };
+ }
+ } else if ( height > bucketHeight ) {
+ // Height fits in the current bucket, resize width accordingly
+ sizes[ bucketName ] = {
+ width: Math.round( bucketHeight * heightToWidth ),
+ height: bucketHeight
+ };
+ }
+ }
+
+ sizes.original = { width: width, height: height };
+
+ return sizes;
+ };
+
+ /**
+ * Generates an i18n message for a label, given a size label and image dimensions
+ *
+ * @param {string} sizeLabel
+ * @param {number} width
+ * @param {number} height
+ * @return {string} i18n label html
+ */
+ RUP.getDimensionsMessageHtml = function ( sizeLabel, width, height ) {
+ var dimensions = this.htmlUtils.jqueryToHtml( $( '<span>' )
+ .addClass( 'mw-mmv-embed-dimensions' )
+ .text(
+ mw.message(
+ 'multimediaviewer-embed-dimensions-separated',
+ mw.message(
+ 'multimediaviewer-embed-dimensions',
+ width, height ).text()
+ ).text()
+ ) );
+
+ return mw.message(
+ 'multimediaviewer-' + sizeLabel + '-embed-dimensions',
+ dimensions
+ ).text();
+ };
+
+ mw.mmv.ui.Utils = Utils;
+}( mediaWiki, jQuery, OO ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.viewingOptions.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.viewingOptions.js
new file mode 100644
index 00000000..61d89722
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.viewingOptions.js
@@ -0,0 +1,397 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $, oo ) {
+ // Shortcut for prototype later
+ var ODP;
+
+ /**
+ * Represents the viewing options dialog and the link to open it.
+ *
+ * @class mw.mmv.ui.OptionsDialog
+ * @extends mw.mmv.ui.Dialog
+ * @param {jQuery} $container the element to which the dialog will be appended
+ * @param {jQuery} $openButton the button which opens the dialog. Only used for positioning.
+ * @param {mw.mmv.Config} config
+ */
+ function OptionsDialog( $container, $openButton, config ) {
+ mw.mmv.ui.Dialog.call( this, $container, $openButton, config );
+
+ this.$dialog.addClass( 'mw-mmv-options-dialog' );
+ this.eventPrefix = 'options';
+
+ this.initPanel();
+ }
+
+ oo.inheritClass( OptionsDialog, mw.mmv.ui.Dialog );
+ ODP = OptionsDialog.prototype;
+
+ ODP.attach = function () {
+ this.handleEvent( 'mmv-options-open', $.proxy( this.handleOpenCloseClick, this ) );
+
+ this.handleEvent( 'mmv-reuse-open', $.proxy( this.closeDialog, this ) );
+ this.handleEvent( 'mmv-download-open', $.proxy( this.closeDialog, this ) );
+ };
+
+ /**
+ * Initialises UI elements.
+ */
+ ODP.initPanel = function () {
+ this.initEnableConfirmation();
+ this.initDisableConfirmation();
+ this.initEnableDiv();
+ this.initDisableDiv();
+ };
+
+ /**
+ * Initialises the enable confirmation pane.
+ */
+ ODP.initEnableConfirmation = function () {
+ this.createConfirmationPane(
+ 'mw-mmv-enable-confirmation',
+ '$enableConfirmation',
+ [
+ mw.message( 'multimediaviewer-enable-confirmation-header' ).text(),
+ mw.message( 'multimediaviewer-enable-confirmation-text', mw.config.get( 'wgSiteName' ) ).text()
+ ] );
+ };
+
+ /**
+ * Initialises the disable confirmation pane.
+ */
+ ODP.initDisableConfirmation = function () {
+ this.createConfirmationPane(
+ 'mw-mmv-disable-confirmation',
+ '$disableConfirmation',
+ [
+ mw.message( 'multimediaviewer-disable-confirmation-header' ).text(),
+ mw.message( 'multimediaviewer-disable-confirmation-text', mw.config.get( 'wgSiteName' ) ).text()
+ ] );
+ };
+
+ /**
+ * Initialises the enable action pane.
+ */
+ ODP.initEnableDiv = function () {
+ this.createActionPane(
+ 'mw-mmv-options-enable',
+ '$enableDiv',
+ mw.message( 'multimediaviewer-enable-submit-button' ).text(),
+ [
+ mw.message( 'multimediaviewer-enable-dialog-header' ).text(),
+ mw.message( 'multimediaviewer-enable-text-header' ).text()
+ ], true );
+ };
+
+ /**
+ * Initialises the disable action pane.
+ */
+ ODP.initDisableDiv = function () {
+ this.createActionPane(
+ 'mw-mmv-options-disable',
+ '$disableDiv',
+ mw.message( 'multimediaviewer-option-submit-button' ).text(),
+ [
+ mw.message( 'multimediaviewer-options-dialog-header' ).text(),
+ mw.message( 'multimediaviewer-options-text-header' ).text(),
+ mw.message( 'multimediaviewer-options-text-body' ).text()
+ ], false );
+ };
+
+ /**
+ * Hides all of the divs.
+ */
+ ODP.hideDivs = function () {
+ this.$dialog.removeClass( 'mw-mmv-disable-confirmation-shown mw-mmv-enable-confirmation-shown mw-mmv-enable-div-shown' );
+
+ this.$disableDiv
+ .add( this.$disableConfirmation )
+ .add( this.$enableDiv )
+ .add( this.$enableConfirmation )
+ .removeClass( 'mw-mmv-shown' );
+ };
+
+ /**
+ * Shows the confirmation div for the disable action.
+ */
+ ODP.showDisableConfirmation = function () {
+ this.hideDivs();
+ this.$disableConfirmation.addClass( 'mw-mmv-shown' );
+ this.$dialog.addClass( 'mw-mmv-disable-confirmation-shown' );
+ };
+
+ /**
+ * Shows the confirmation div for the enable action.
+ */
+ ODP.showEnableConfirmation = function () {
+ this.hideDivs();
+ this.$enableConfirmation.addClass( 'mw-mmv-shown' );
+ this.$dialog.addClass( 'mw-mmv-enable-confirmation-shown' );
+ };
+
+ /**
+ * @event mmv-options-opened
+ * Fired when the dialog is opened.
+ */
+
+ /**
+ * Opens a dialog with information about file reuse.
+ */
+ ODP.openDialog = function () {
+ if ( this.isEnabled() ) {
+ this.$disableDiv.addClass( 'mw-mmv-shown' );
+ } else {
+ this.$enableDiv.addClass( 'mw-mmv-shown' );
+ this.$dialog.addClass( 'mw-mmv-enable-div-shown' );
+ }
+
+ mw.mmv.ui.Dialog.prototype.openDialog.call( this );
+ $( document ).trigger( 'mmv-options-opened' );
+ };
+
+ /**
+ * @event mmv-options-closed
+ * Fired when the dialog is closed.
+ */
+
+ /**
+ * Closes the options dialog.
+ *
+ * @param {Event} [e] Event object when the close action is caused by a user
+ * action, as opposed to closing the window or something.
+ */
+ ODP.closeDialog = function ( e ) {
+ var wasConfirmation = this.$dialog.is( '.mw-mmv-disable-confirmation-shown' ) || this.$dialog.is( '.mw-mmv-enable-confirmation-shown' );
+
+ mw.mmv.ui.Dialog.prototype.closeDialog.call( this );
+ $( document ).trigger( 'mmv-options-closed' );
+ this.hideDivs();
+
+ if ( e && $( e.target ).is( '.mw-mmv-options-button' ) && wasConfirmation ) {
+ this.openDialog();
+ }
+ };
+
+ /**
+ * Creates a confirmation pane.
+ *
+ * @param {string} divClass Class applied to main div.
+ * @param {string} propName Name of the property on this object to which we'll assign the div.
+ * @param {string} msgs See #addText
+ */
+ ODP.createConfirmationPane = function ( divClass, propName, msgs ) {
+ var dialog = this,
+ $div = $( '<div>' )
+ .addClass( divClass )
+ .appendTo( this.$dialog );
+
+ $( '<div>' )
+ .html( '&nbsp;' )
+ .addClass( 'mw-mmv-confirmation-close' )
+ .click( function () {
+ dialog.closeDialog();
+ } )
+ .appendTo( $div );
+
+ this.addText( $div, msgs );
+
+ this[ propName ] = $div;
+ };
+
+ /**
+ * Creates an action pane.
+ *
+ * @param {string} divClass Class applied to main div.
+ * @param {string} propName Name of the property on this object to which we'll assign the div.
+ * @param {string} smsg Message for the submit button.
+ * @param {string} msgs See #addText
+ * @param {boolean} enabled Whether this dialog is an enable one.
+ */
+ ODP.createActionPane = function ( divClass, propName, smsg, msgs, enabled ) {
+ var $div = $( '<div>' )
+ .addClass( divClass )
+ .appendTo( this.$dialog );
+
+ if ( enabled ) {
+ $( '<div>' )
+ .addClass( 'mw-mmv-options-enable-alert' )
+ .text( mw.message( 'multimediaviewer-enable-alert' ).text() )
+ .appendTo( $div );
+ }
+
+ this.addText( $div, msgs, true );
+ this.addInfoLink( $div, ( enabled ? 'enable' : 'disable' ) + '-about-link' );
+ this.makeButtons( $div, smsg, enabled );
+
+ this[ propName ] = $div;
+ };
+
+ /**
+ * Creates buttons for the dialog.
+ *
+ * @param {jQuery} $container
+ * @param {string} smsg Message for the submit button.
+ * @param {boolean} enabled Whether the viewer is enabled after this dialog is submitted.
+ */
+ ODP.makeButtons = function ( $container, smsg, enabled ) {
+ var $submitDiv = $( '<div>' )
+ .addClass( 'mw-mmv-options-submit' )
+ .appendTo( $container );
+
+ this.makeSubmitButton(
+ $submitDiv,
+ smsg,
+ enabled
+ );
+
+ this.makeCancelButton( $submitDiv );
+ };
+
+ /**
+ * Makes a submit button for one of the panels.
+ *
+ * @param {jQuery} $submitDiv The div for the buttons in the dialog.
+ * @param {string} msg The string to put in the button.
+ * @param {boolean} enabled Whether to turn the viewer on or off when this button is pressed.
+ * @return {jQuery} Submit button
+ */
+ ODP.makeSubmitButton = function ( $submitDiv, msg, enabled ) {
+ var dialog = this;
+
+ return $( '<button>' )
+ .addClass( 'mw-mmv-options-submit-button mw-ui-button mw-ui-progressive' )
+ .text( msg )
+ .appendTo( $submitDiv )
+ .click( function () {
+ var $buttons = $( this ).closest( '.mw-mmv-options-submit' ).find( '.mw-mmv-options-submit-button, .mw-mmv-options-cancel-button' );
+ $buttons.prop( 'disabled', true );
+
+ dialog.config.setMediaViewerEnabledOnClick( enabled ).done( function () {
+ mw.mmv.actionLogger.log( 'opt' + ( enabled ? 'in' : 'out' ) + '-' + ( mw.user.isAnon() ? 'anon' : 'loggedin' ) );
+
+ if ( enabled ) {
+ dialog.showEnableConfirmation();
+ } else {
+ dialog.showDisableConfirmation();
+ }
+ } ).always( function () {
+ $buttons.prop( 'disabled', false );
+ } );
+
+ return false;
+ } );
+ };
+
+ /**
+ * Makes a cancel button for one of the panels.
+ *
+ * @param {jQuery} $submitDiv The div for the buttons in the dialog.
+ * @return {jQuery} Cancel button
+ */
+ ODP.makeCancelButton = function ( $submitDiv ) {
+ var dialog = this;
+
+ return $( '<button>' )
+ .addClass( 'mw-mmv-options-cancel-button mw-ui-button mw-ui-quiet' )
+ .text( mw.message( 'multimediaviewer-option-cancel-button' ).text() )
+ .appendTo( $submitDiv )
+ .click( function () {
+ dialog.closeDialog();
+ return false;
+ } );
+ };
+
+ /**
+ * Adds text to a dialog.
+ *
+ * @param {jQuery} $container
+ * @param {string[]} msgs The messages to be added.
+ * @param {boolean} icon Whether to display an icon next to the text or not
+ */
+ ODP.addText = function ( $container, msgs, icon ) {
+ var i, $text, $subContainer,
+ adders = [
+ function ( msg ) {
+ $( '<h3>' )
+ .text( msg )
+ .addClass( 'mw-mmv-options-dialog-header' )
+ .appendTo( $container );
+ },
+
+ function ( msg ) {
+ $( '<p>' )
+ .text( msg )
+ .addClass( 'mw-mmv-options-text-header' )
+ .appendTo( $text );
+ },
+
+ function ( msg ) {
+ $( '<p>' )
+ .text( msg )
+ .addClass( 'mw-mmv-options-text-body' )
+ .appendTo( $text );
+ }
+ ];
+
+ $text = $( '<div>' )
+ .addClass( 'mw-mmv-options-text' );
+
+ for ( i = 0; i < msgs.length && i < adders.length; i++ ) {
+ adders[ i ]( msgs[ i ] );
+ }
+
+ if ( icon ) {
+ $subContainer = $( '<div>' ).addClass( 'mw-mmv-options-subcontainer' );
+
+ $( '<div>' )
+ .html( '&nbsp;' )
+ .addClass( 'mw-mmv-options-icon' )
+ .appendTo( $subContainer );
+
+ $text.appendTo( $subContainer );
+ $subContainer.appendTo( $container );
+ } else {
+ $text.appendTo( $container );
+ }
+ };
+
+ /**
+ * Adds the info link to the panel.
+ *
+ * @param {jQuery} $div The panel to which we're adding the link.
+ * @param {string} eventName
+ */
+ ODP.addInfoLink = function ( $div, eventName ) {
+ $( '<a>' )
+ .addClass( 'mw-mmv-project-info-link' )
+ .prop( 'href', mw.config.get( 'wgMultimediaViewer' ).helpLink )
+ .text( mw.message( 'multimediaviewer-options-learn-more' ) )
+ .click( function () { mw.mmv.actionLogger.log( eventName ); } )
+ .appendTo( $div.find( '.mw-mmv-options-text' ) );
+ };
+
+ /**
+ * Checks the preference.
+ *
+ * @return {boolean} MV is enabled
+ */
+ ODP.isEnabled = function () {
+ return this.config.isMediaViewerEnabledOnClick();
+ };
+
+ mw.mmv.ui.OptionsDialog = OptionsDialog;
+}( mediaWiki, jQuery, OO ) );
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.viewingOptions.less b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.viewingOptions.less
new file mode 100644
index 00000000..7112172c
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/ui/mmv.ui.viewingOptions.less
@@ -0,0 +1,160 @@
+@import '../mmv.globals';
+@import '../mmv.mixins';
+
+.mw-mmv-options-dialog {
+ @offset-top: ( @buttons-offset-right + ( 2 * @buttons-offset-each-top ) + 6px );
+
+ top: @offset-top;
+ height: auto;
+ z-index: 1004;
+ padding: 15px;
+
+ &.mw-mmv-enable-confirmation-shown {
+ background-color: #00af89;
+ box-shadow: 0 2px 0 #00634e;
+
+ .mw-mmv-dialog-down-arrow {
+ background-color: #00af89;
+ }
+ }
+
+ &.mw-mmv-disable-confirmation-shown,
+ &.mw-mmv-disable-confirmation-shown .mw-mmv-dialog-down-arrow,
+ &.mw-mmv-enable-div-shown .mw-mmv-dialog-down-arrow {
+ background-color: #eaecf0;
+ }
+
+ .mw-mmv-dialog-down-arrow {
+ @arrow-size: 20px;
+ top: ( @offset-top + ( @arrow-size / 2 ) );
+ }
+
+ .mw-mmv-enable-confirmation,
+ .mw-mmv-disable-confirmation,
+ .mw-mmv-options-enable,
+ .mw-mmv-options-disable {
+ position: relative;
+ display: none;
+
+ &.mw-mmv-shown {
+ display: block;
+ }
+ }
+
+ .mw-mmv-confirmation-close {
+ display: inline-block;
+ position: absolute;
+ right: 0;
+ top: 0;
+ width: 15px;
+ height: 15px;
+ cursor: pointer;
+ opacity: 0.75;
+ /* @embed */
+ background-image: url( img/x_white.svg );
+ background-size: 15px 15px;
+
+ &:hover {
+ opacity: 1;
+ }
+ }
+
+ .mw-mmv-disable-confirmation {
+ .mw-mmv-options-dialog-header {
+ color: #222;
+ padding: 0;
+ }
+
+ .mw-mmv-options-text-header {
+ color: #222;
+ }
+ }
+
+ .mw-mmv-enable-confirmation {
+ .mw-mmv-options-dialog-header {
+ color: #fff;
+ padding: 0;
+ }
+
+ .mw-mmv-options-text-header {
+ color: #fff;
+ }
+ }
+
+ .mw-mmv-disable-confirmation,
+ .mw-mmv-enable-confirmation {
+ padding: 0;
+
+ .mw-mmv-options-text {
+ left: 0;
+ }
+ }
+}
+
+.mw-mmv-options-text,
+.mw-mmv-options-icon {
+ .mw-mmv-options-enable & {
+ top: 70px;
+ }
+}
+
+.mw-mmv-options-submit {
+ margin-top: 10px;
+}
+
+.mw-mmv-options-text {
+ left: 68px;
+ right: 0;
+}
+
+.mw-mmv-options-subcontainer .mw-mmv-options-text {
+ margin-left: 68px;
+}
+
+.mw-mmv-options-icon {
+ /* @embed */
+ background-image: url( img/icon_mmv.svg );
+ float: left;
+ width: 58px;
+ height: 52px;
+}
+
+.mw-mmv-options-cancel-button,
+.mw-mmv-options-submit-button {
+ float: right;
+}
+
+.mw-mmv-options-dialog-header {
+ padding-top: 0;
+ font-weight: normal;
+ font-size: 1.25em;
+ color: #222;
+
+ .mw-mmv-options-enable & {
+ top: 35px;
+ }
+}
+
+.mw-mmv-options-text-header {
+ margin: 0;
+ font-size: 1em;
+ color: #54595d;
+}
+
+.mw-mmv-options-text-body {
+ font-size: 0.9em;
+ color: #72777d;
+}
+
+.mw-mmv-options-enable-alert {
+ background-color: #eaecf0;
+ color: #222;
+ position: absolute;
+ left: -15px;
+ right: -15px;
+ top: -15px;
+ border-top-right-radius: @border-radius;
+ border-top-left-radius: @border-radius;
+ padding: 10px 15px;
+ font-weight: 500;
+}
diff --git a/www/wiki/extensions/MultimediaViewer/tests/browser/LocalSettings.php b/www/wiki/extensions/MultimediaViewer/tests/browser/LocalSettings.php
new file mode 100644
index 00000000..acd153c6
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/browser/LocalSettings.php
@@ -0,0 +1,2 @@
+<?php
+ $wgUseInstantCommons = true;
diff --git a/www/wiki/extensions/MultimediaViewer/tests/browser/README.md b/www/wiki/extensions/MultimediaViewer/tests/browser/README.md
new file mode 100644
index 00000000..36319498
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/browser/README.md
@@ -0,0 +1 @@
+Please see https://github.com/wikimedia/mediawiki-selenium for instructions on how to run tests.
diff --git a/www/wiki/extensions/MultimediaViewer/tests/browser/ci.yml b/www/wiki/extensions/MultimediaViewer/tests/browser/ci.yml
new file mode 100644
index 00000000..b95ed6f9
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/browser/ci.yml
@@ -0,0 +1,28 @@
+BROWSER:
+ - chrome
+ - firefox
+ - safari
+
+MEDIAWIKI_ENVIRONMENT:
+ - beta
+ - mediawiki
+
+PLATFORM:
+ - Linux
+ - OS X 10.9
+
+exclude:
+ - BROWSER: chrome
+ MEDIAWIKI_ENVIRONMENT: mediawiki
+
+ - BROWSER: chrome
+ PLATFORM: Linux
+
+ - BROWSER: firefox
+ PLATFORM: OS X 10.9
+
+ - BROWSER: safari
+ MEDIAWIKI_ENVIRONMENT: mediawiki
+
+ - BROWSER: safari
+ PLATFORM: Linux
diff --git a/www/wiki/extensions/MultimediaViewer/tests/browser/environments.yml b/www/wiki/extensions/MultimediaViewer/tests/browser/environments.yml
new file mode 100644
index 00000000..caa5168e
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/browser/environments.yml
@@ -0,0 +1,42 @@
+# Customize this configuration as necessary to provide defaults for various
+# test environments.
+#
+# The set of defaults to use is determined by the MEDIAWIKI_ENVIRONMENT
+# environment variable.
+#
+# export MEDIAWIKI_ENVIRONMENT=mw-vagrant-host
+# bundle exec cucumber
+#
+# Additional variables set by the environment will override the corresponding
+# defaults defined here.
+#
+# export MEDIAWIKI_ENVIRONMENT=mw-vagrant-host
+# export MEDIAWIKI_USER=Selenium_user2
+# bundle exec cucumber
+#
+mw-vagrant-host: &default
+ browser_useragent: test-user-agent
+ user_factory: true
+ mediawiki_url: http://127.0.0.1:8080/wiki/
+
+mw-vagrant-guest:
+ user_factory: true
+ mediawiki_url: http://127.0.0.1/wiki/
+
+beta:
+ browser_useragent: test-user-agent
+ mediawiki_url: https://en.wikipedia.beta.wmflabs.org/wiki/
+ mediawiki_user: Selenium_user
+ # mediawiki_password: SET THIS IN THE ENVIRONMENT!
+
+mediawiki:
+ browser_useragent: test-user-agent
+ mediawiki_url: https://www.mediawiki.org/wiki/
+ mediawiki_user: Selenium_user
+ # mediawiki_password: SET THIS IN THE ENVIRONMENT!
+
+integration:
+ user_factory: true
+ # mediawiki_url: THIS WILL BE SET BY JENKINS
+
+default: *default
diff --git a/www/wiki/extensions/MultimediaViewer/tests/browser/features/mmv.download.feature b/www/wiki/extensions/MultimediaViewer/tests/browser/features/mmv.download.feature
new file mode 100644
index 00000000..fac69f34
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/browser/features/mmv.download.feature
@@ -0,0 +1,59 @@
+@chrome @en.wikipedia.beta.wmflabs.org @firefox @integration @safari @test2.wikipedia.org
+Feature: Download menu
+
+ Background:
+ Given I am viewing an image using MMV
+
+ Scenario: Download menu can be opened
+ When I click the download icon
+ Then the download menu should appear
+
+ Scenario: Clicking the image closes the download menu
+ When I click the download icon
+ And the download menu appears
+ And I click the image
+ Then the download menu should disappear
+
+ Scenario: Image size defaults to original
+ When I click the download icon
+ Then the original beginning download image size label should be "4000 × 3000 px jpg"
+ And the download links should be the original image
+
+ Scenario: Attribution area is collapsed by default
+ When I click the download icon
+ Then the attribution area should be collapsed
+
+ Scenario: Attribution area can be opened
+ When I click the download icon
+ And I click on the attribution area
+ Then the attribution area should be open
+
+ Scenario: Attribution area can be closed
+ When I click the download icon
+ And I click on the attribution area
+ And I click on the attribution area close icon
+ Then the attribution area should be collapsed
+
+ Scenario: The small download option has the correct information
+ When I open the download dropdown
+ And the download size options appear
+ And I click the small download size
+ And the download size options disappears
+ Then the download image size label should be "193 × 145 px jpg"
+ And the download links should be the 193 thumbnail
+
+ Scenario: The medium download option has the correct information
+ When I open the download dropdown
+ And the download size options appear
+ And I click the medium download size
+ And the download size options disappears
+ Then the download image size label should be "640 × 480 px jpg"
+ And the download links should be the 640 thumbnail
+
+ Scenario: The large download option has the correct information
+ When I open the download dropdown
+ And the download size options appear
+ And I click the large download size
+ And the download size options disappears
+ Then the download image size label should be "1200 × 900 px jpg"
+ And the download links should be the 1200 thumbnail
diff --git a/www/wiki/extensions/MultimediaViewer/tests/browser/features/mmv.navigation.feature b/www/wiki/extensions/MultimediaViewer/tests/browser/features/mmv.navigation.feature
new file mode 100644
index 00000000..39fae447
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/browser/features/mmv.navigation.feature
@@ -0,0 +1,23 @@
+@chrome @en.wikipedia.beta.wmflabs.org @firefox @integration @test2.wikipedia.org
+Feature: Navigation
+
+ Background:
+ Given I am viewing an image using MMV
+
+ Scenario: Clicking the next arrow takes me to the next image
+ When I click the next arrow
+ Then the image and metadata of the next image should appear
+
+ Scenario: Clicking the previous arrow takes me to the previous image
+ When I click the previous arrow
+ Then the image and metadata of the previous image should appear
+
+ Scenario: Closing MMV restores the scroll position
+ When I close MMV
+ Then I should be navigated back to the original wiki article
+ And the wiki article should be scrolled to the same position as before opening MMV
+
+ Scenario: Browsing back to close MMV restores the scroll position
+ When I press the browser back button
+ Then I should be navigated back to the original wiki article
+ And the wiki article should be scrolled to the same position as before opening MMV
diff --git a/www/wiki/extensions/MultimediaViewer/tests/browser/features/mmv.options.feature b/www/wiki/extensions/MultimediaViewer/tests/browser/features/mmv.options.feature
new file mode 100644
index 00000000..85c6826d
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/browser/features/mmv.options.feature
@@ -0,0 +1,44 @@
+@chrome @en.wikipedia.beta.wmflabs.org @firefox @integration @test2.wikipedia.org
+Feature: Options
+
+ Background:
+ Given I am viewing an image using MMV
+
+ Scenario: Clicking the X icon on the enable confirmation closes the options menu
+ Given I reenable MMV
+ When I click the enable X icon
+ Then the options menu should disappear
+
+ Scenario: Clicking the X icon on the disable confirmation closes the options menu
+ Given I disable MMV
+ When I click the disable X icon
+ Then the options menu should disappear
+
+ Scenario: Clicking the image closes the options menu
+ Given I click the options icon
+ When I click the image
+ Then the options menu should disappear
+
+ Scenario: Clicking cancel closes the options menu
+ Given I click the options icon
+ When I click the disable cancel button
+ Then the options menu should disappear
+
+ Scenario: Clicking the options icon brings up the options menu
+ When I click the options icon
+ Then the options menu should appear with the prompt to disable
+
+ Scenario: Clicking enable shows the confirmation
+ Given I click the options icon with MMV disabled
+ When I click the enable button
+ Then the enable confirmation should appear
+
+ Scenario: Clicking disable shows the confirmation
+ Given I click the options icon
+ When I click the disable button
+ Then the disable confirmation should appear
+
+ Scenario: Disabling media viewer makes the next thumbnail click go to the file page
+ Given I disable and close MMV
+ When I click on the first image in the article
+ Then I am taken to the file page
diff --git a/www/wiki/extensions/MultimediaViewer/tests/browser/features/mmv.performance.feature b/www/wiki/extensions/MultimediaViewer/tests/browser/features/mmv.performance.feature
new file mode 100644
index 00000000..37e0c658
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/browser/features/mmv.performance.feature
@@ -0,0 +1,42 @@
+@en.wikipedia.beta.wmflabs.org @firefox @www.mediawiki.org @test2.wikipedia.org
+Feature: Multimedia Viewer performance
+
+ Background:
+ Given I am using a custom user agent
+ And I am at a wiki article with at least two embedded pictures
+
+ Scenario: Commons with warm cache
+ Given I visit an unrelated Commons page to warm up the browser cache
+ And I visit the Commons page
+ Then the File: page image is loaded
+
+ Scenario: MMV with warm cache and small browser window
+ Given I have a small browser window
+ When I click on an unrelated image in the article to warm up the browser cache
+ And I close MMV
+ And I click on the first image in the article
+ Then the MMV image is loaded in 125 percent of the time with a warm cache and an average browser window
+
+ Scenario: MMV with cold cache and average browser window
+ Given I have an average browser window
+ When I click on the first image in the article
+ Then the MMV image is loaded in 210 percent of the time with a cold cache and an average browser window
+
+ Scenario: MMV with warm cache and average browser window
+ Given I have an average browser window
+ When I click on an unrelated image in the article to warm up the browser cache
+ And I close MMV
+ And I click on the first image in the article
+ Then the MMV image is loaded in 125 percent of the time with a warm cache and an average browser window
+
+ Scenario: MMV with cold cache and large browser window
+ Given I have a large browser window
+ When I click on the first image in the article
+ Then the MMV image is loaded in 240 percent of the time with a cold cache and a large browser window
+
+ Scenario: MMV with warm cache and large browser window
+ Given I have a large browser window
+ When I click on an unrelated image in the article to warm up the browser cache
+ And I close MMV
+ And I click on the first image in the article
+ Then the MMV image is loaded in 125 percent of the time with a warm cache and a large browser window
diff --git a/www/wiki/extensions/MultimediaViewer/tests/browser/features/step_definitions/mmv_download_steps.rb b/www/wiki/extensions/MultimediaViewer/tests/browser/features/step_definitions/mmv_download_steps.rb
new file mode 100644
index 00000000..1d94a804
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/browser/features/step_definitions/mmv_download_steps.rb
@@ -0,0 +1,101 @@
+# encoding: utf-8
+
+When /^I open the download dropdown$/ do
+ step 'I click the download icon'
+ step 'I click the download down arrow icon'
+end
+
+When /^I click the download icon$/ do
+ on(E2ETestPage).mmv_download_icon_element.when_present.click
+end
+
+When /^I click the download down arrow icon$/ do
+ sleep 1
+ on(E2ETestPage).mmv_download_down_arrow_icon_element.when_present(10).click
+end
+
+When /^I click on the attribution area$/ do
+ on(E2ETestPage).mmv_download_attribution_area_element.when_present(10).click
+end
+
+When /^I click on the attribution area close icon$/ do
+ on(E2ETestPage).mmv_download_attribution_area_close_icon_element.click
+end
+
+When /^I click the (.*) download size$/ do |size_option|
+ on(E2ETestPage) do |page|
+ case size_option
+ when 'small'
+ @index = 1
+ when 'medium'
+ @index = 2
+ when 'large'
+ @index = 3
+ else
+ @index = 0
+ end
+
+ page.mmv_download_size_options_elements[@index].click
+ end
+end
+
+When /^the download size options appear$/ do
+ on(E2ETestPage).mmv_download_size_menu_element.when_present
+end
+
+When /^the download size options disappears$/ do
+ on(E2ETestPage).mmv_download_size_menu_element.when_not_present
+end
+
+When /^the download menu appears$/ do
+ on(E2ETestPage).mmv_download_menu_element.when_present(10)
+end
+
+Then /^the download menu should appear$/ do
+ expect(on(E2ETestPage).mmv_download_menu_element.when_present(10)).to be_visible
+end
+
+Then /^the download menu should disappear$/ do
+ expect(on(E2ETestPage).mmv_download_menu_element).not_to be_visible
+end
+
+Then /^the original beginning download image size label should be "(.*)"$/ do |size_in_pixels|
+ expect(on(E2ETestPage).mmv_download_size_label_element.when_present(10).text).to eq size_in_pixels
+end
+
+Then /^the download image size label should be "(.*)"$/ do |size_in_pixels|
+ on(E2ETestPage) do |page|
+ page.mmv_download_size_options_elements[0].when_not_present
+ expect(page.mmv_download_size_label_element.when_present.text).to eq size_in_pixels
+ end
+end
+
+Then /^the download size options should appear$/ do
+ expect(on(E2ETestPage).mmv_download_size_menu_element.when_present).to be_visible
+end
+
+Then /^the download links should be the original image$/ do
+ on(E2ETestPage) do |page|
+ expect(page.mmv_download_link_element.attribute('href')).to match /^?download$/
+ expect(page.mmv_download_preview_link_element.attribute('href')).not_to match /^?download$/
+ expect(page.mmv_download_link_element.attribute('href')).not_to match %r{/thumb/}
+ expect(page.mmv_download_preview_link_element.attribute('href')).not_to match %r{/thumb/}
+ end
+end
+
+Then /^the download links should be the (\d+) thumbnail$/ do |thumb_size|
+ on(E2ETestPage) do |page|
+ page.wait_until { page.mmv_download_link_element.attribute('href').match thumb_size }
+ expect(page.mmv_download_link_element.attribute('href')).to match /^?download$/
+ expect(page.mmv_download_preview_link_element.attribute('href')).not_to match /^?download$/
+ expect(page.mmv_download_preview_link_element.attribute('href')).to match thumb_size
+ end
+end
+
+Then /^the attribution area should be collapsed$/ do
+ expect(on(E2ETestPage).mmv_download_attribution_area_element.when_present(10).attribute('class')).to match 'mw-mmv-download-attribution-collapsed'
+end
+
+Then /^the attribution area should be open$/ do
+ expect(on(E2ETestPage).mmv_download_attribution_area_element.when_present.attribute('class')).not_to match 'mw-mmv-download-attribution-collapsed'
+end
diff --git a/www/wiki/extensions/MultimediaViewer/tests/browser/features/step_definitions/mmv_navigation_steps.rb b/www/wiki/extensions/MultimediaViewer/tests/browser/features/step_definitions/mmv_navigation_steps.rb
new file mode 100644
index 00000000..bd73d899
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/browser/features/step_definitions/mmv_navigation_steps.rb
@@ -0,0 +1,44 @@
+# encoding: utf-8
+
+When /^I click the next arrow$/ do
+ on(E2ETestPage).mmv_next_button_element.when_present.click
+end
+
+When /^I click the previous arrow$/ do
+ on(E2ETestPage).mmv_previous_button_element.when_present.click
+end
+
+When /^I press the browser back button$/ do
+ # $browser.back doesn't work for Safari. This is a workaround for https://code.google.com/p/selenium/issues/detail?id=3771
+ on(E2ETestPage).execute_script('window.history.back();')
+end
+
+Then /^the image and metadata of the next image should appear$/ do
+ on(E2ETestPage) do |page|
+ # MMV was launched, article is not visible yet
+ expect(page.image1_in_article_element).not_to be_visible
+ check_elements_in_viewer_for_image3 page
+ end
+end
+
+Then /^the image and metadata of the previous image should appear$/ do
+ on(E2ETestPage) do |page|
+ # MMV was launched, article is not visible yet
+ expect(page.image1_in_article_element).not_to be_visible
+ check_elements_in_viewer_for_image1 page
+ end
+end
+
+Then /^the wiki article should be scrolled to the same position as before opening MMV$/ do
+ on(E2ETestPage) do |page|
+ scroll_difference = page.execute_script('return $(window).scrollTop();') - @article_scroll_top
+ expect(scroll_difference.abs).to be < 2
+ end
+end
+
+Then /^I should be navigated back to the original wiki article$/ do
+ on(E2ETestPage) do |page|
+ expect(page.image1_in_article_element).to be_visible
+ expect(page.mmv_wrapper_element).not_to be_visible
+ end
+end
diff --git a/www/wiki/extensions/MultimediaViewer/tests/browser/features/step_definitions/mmv_options_steps.rb b/www/wiki/extensions/MultimediaViewer/tests/browser/features/step_definitions/mmv_options_steps.rb
new file mode 100644
index 00000000..db5c380c
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/browser/features/step_definitions/mmv_options_steps.rb
@@ -0,0 +1,69 @@
+# encoding: utf-8
+
+When /^I click the options icon$/ do
+ on(E2ETestPage).mmv_options_icon_element.click
+end
+
+Then /^the options menu should appear with the prompt to disable$/ do
+ on(E2ETestPage).mmv_options_menu_disable_element.should be_visible
+end
+
+Then /^the options menu should disappear$/ do
+ on(E2ETestPage).mmv_options_menu_disable_element.should_not be_visible
+end
+
+When /^I click the enable button$/ do
+ on(E2ETestPage).mmv_options_enable_button_element.click
+end
+
+When /^I click the disable button$/ do
+ on(E2ETestPage).mmv_options_disable_button_element.click
+end
+
+When /^I click the disable cancel button$/ do
+ on(E2ETestPage).mmv_options_disable_cancel_button_element.click
+end
+
+When /^I click the enable X icon$/ do
+ on(E2ETestPage).mmv_options_enable_x_icon_element.click
+end
+
+When /^I click the disable X icon$/ do
+ on(E2ETestPage).mmv_options_disable_x_icon_element.click
+end
+
+When /^I disable MMV$/ do
+ step 'I click the options icon'
+ step 'I click the disable button'
+end
+
+When /^I reenable MMV$/ do
+ step 'I disable MMV'
+ step 'I click the options icon'
+ step 'I click the enable button'
+end
+
+When /^I click the options icon with MMV disabled$/ do
+ step 'I disable MMV'
+ step 'I click the options icon'
+end
+
+When /^I disable and close MMV$/ do
+ step 'I disable MMV'
+ step 'I close MMV'
+end
+
+Then /^the disable confirmation should appear$/ do
+ on(E2ETestPage).mmv_options_disable_confirmation_element.should be_visible
+end
+
+Then /^the enable confirmation should appear$/ do
+ on(E2ETestPage).mmv_options_enable_confirmation_element.should be_visible
+end
+
+Then /^I am taken to the file page$/ do
+ on(E2ETestPage) do |page|
+ page.current_url.should match %r{/File:}
+ page.current_url.should_not match %r{#/media}
+ end
+end
diff --git a/www/wiki/extensions/MultimediaViewer/tests/browser/features/step_definitions/mmv_performance_steps.rb b/www/wiki/extensions/MultimediaViewer/tests/browser/features/step_definitions/mmv_performance_steps.rb
new file mode 100644
index 00000000..c81ee4e1
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/browser/features/step_definitions/mmv_performance_steps.rb
@@ -0,0 +1,48 @@
+When /^I click on an unrelated image in the article to warm up the browser cache$/ do
+ on(E2ETestPage).other_image_in_article
+end
+
+Given /^I visit the Commons page$/ do
+ @commons_open_time = Time.now.getutc
+ browser.goto 'https://commons.wikimedia.org/wiki/File:Sunrise_over_fishing_boats_in_Kerala.jpg'
+end
+
+Given /^I visit an unrelated Commons page to warm up the browser cache$/ do
+ browser.goto 'https://commons.wikimedia.org/wiki/File:Wikimedia_Foundation_2013_All_Hands_Offsite_-_Day_2_-_Photo_16.jpg'
+end
+
+Given /^I have a small browser window$/ do
+ browser.window.resize_to 900, 700
+end
+
+Given /^I have an average browser window$/ do
+ browser.window.resize_to 1366, 768
+end
+
+Given /^I have a large browser window$/ do
+ browser.window.resize_to 1920, 1080
+end
+
+Given /^I am using a custom user agent$/ do
+ browser_factory.override(browser_user_agent: env[:browser_useragent])
+end
+
+Then /^the File: page image is loaded$/ do
+ on(CommonsPage) do |page|
+ page.wait_for_image_load '.fullImageLink img'
+ # Has to be a global variable, otherwise it doesn't survive between scenarios
+ $commons_time = Time.now.getutc - @commons_open_time
+ page.log_performance type: 'file-page', duration: $commons_time * 1000
+ end
+end
+
+Then /^the MMV image is loaded in (\d+) percent of the time with a (.*) cache and an? (.*) browser window$/ do |percentage, cache, window_size|
+ on(E2ETestPage) do |page|
+ page.wait_for_image_load '.mw-mmv-image img'
+ mmv_time = Time.now.getutc - @image_click_time
+ page.log_performance type: 'mmv', duration: mmv_time * 1000, cache: cache, windowSize: window_size
+
+ expected_time = $commons_time * (percentage.to_f / 100.0)
+ expect(mmv_time).to be < expected_time
+ end
+end
diff --git a/www/wiki/extensions/MultimediaViewer/tests/browser/features/step_definitions/mmv_steps.rb b/www/wiki/extensions/MultimediaViewer/tests/browser/features/step_definitions/mmv_steps.rb
new file mode 100644
index 00000000..3e4590bc
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/browser/features/step_definitions/mmv_steps.rb
@@ -0,0 +1,174 @@
+# encoding: utf-8
+
+Given /^I am at a wiki article with at least two embedded pictures$/ do
+ api.create_page 'MediaViewerE2ETest', File.read(File.join(__dir__, '../../samples/MediaViewerE2ETest.wikitext'))
+ visit(E2ETestPage)
+ on(E2ETestPage).image1_in_article_element.when_present.should be_visible
+end
+
+Given /^the MMV has loaded$/ do
+ on(E2ETestPage) do |page|
+ page.wait_until do
+ # Wait for JS to hijack standard link
+ # TODO: If this approach works well, we should implement general
+ # `wait_for_resource` and `resource_ready?` helper methods in
+ # mw-selenium, and document this pattern on mw.org
+ browser.execute_script("return mw.loader.getState('mmv.bootstrap') === 'ready'")
+ end
+ end
+end
+
+Given /^I am viewing an image using MMV$/ do
+ step 'I am at a wiki article with at least two embedded pictures'
+ step 'the MMV has loaded'
+ step 'I click on the second image in the article'
+ step 'the image metadata and the image itself should be there'
+end
+
+When /^I click on the first image in the article$/ do
+ on(E2ETestPage) do |page|
+ # We store the offset of the image as the scroll position and scroll to it, because cucumber/selenium
+ # sometimes automatically scrolls to it when we ask it to click on it (seems to depend on timing)
+ @article_scroll_top = page.execute_script("var scrollTop = Math.round($('a[href$=\"File:Sunrise_over_fishing_boats_in_Kerala.jpg\"]').first().find('img').offset().top); window.scrollTo(0, scrollTop); return scrollTop;")
+ # Scrolls to the image and clicks on it
+ page.image1_in_article
+ # This is a global variable that can be used to measure performance
+ @image_click_time = Time.now.getutc
+ end
+end
+
+When /^I click on the second image in the article$/ do
+ on(E2ETestPage) do |page|
+ # We store the offset of the image as the scroll position and scroll to it, because cucumber/selenium
+ # sometimes automatically scrolls to it when we ask it to click on it (seems to depend on timing)
+ @article_scroll_top = page.execute_script("var scrollTop = Math.round($('a[href$=\"File:Wikimedia_Foundation_2013_All_Hands_Offsite_-_Day_2_-_Photo_24.jpg\"]').first().find('img').offset().top); window.scrollTo(0, scrollTop); return scrollTop;")
+ # Scrolls to the image and clicks on it
+ page.image2_in_article
+ # This is a global variable that can be used to measure performance
+ @image_click_time = Time.now.getutc
+ end
+end
+
+When /^I close MMV$/ do
+ on(E2ETestPage).mmv_close_button_element.when_present(30).click
+end
+
+When /^I click the image$/ do
+ on(E2ETestPage) do
+ # Clicking the top-left corner of the image is necessary for the test to work on IE
+ # A plain click on the image element ends up hitting the dialog, which means it won't close
+ begin
+ browser.driver.action.move_to(browser.driver.find_element(:class, 'mw-mmv-image'), 10, 10).click.perform
+ rescue
+ # Plain click for web drivers that don't support mouse moves (Safari, currently)
+ on(E2ETestPage).mmv_image_div_element.when_present.click
+ end
+ end
+end
+
+Then /^the image metadata and the image itself should be there$/ do
+ on(E2ETestPage) do |page|
+ # MMV was launched, article is not visible now
+ page.image1_in_article_element.should_not be_visible
+ check_elements_in_viewer_for_image2 page
+ end
+end
+
+# Helper function that verifies the presence of various elements in viewer
+# while looking at image1 (Kerala)
+def check_elements_in_viewer_for_image1(page)
+ # Check basic MMV elements are present
+ expect(page.mmv_overlay_element.when_present).to be_visible
+ expect(page.mmv_wrapper_element.when_present).to be_visible
+ expect(page.mmv_image_div_element).to be_visible
+
+ # Check image content
+ expect(page.mmv_final_image_element.when_present.attribute('src')).to match /Kerala/
+
+ # Check basic metadata is present
+
+ # Title
+ expect(page.mmv_metadata_title_element.when_present.text).to match /^Sunrise over fishing boats$/
+ # License
+ expect(page.mmv_metadata_license_element.when_present.attribute('href')).to match %r{^https?://creativecommons.org/licenses/by-sa/3.0$}
+ expect(page.mmv_metadata_license_element.when_present.text).to match 'CC BY-SA 3.0'
+ # Credit
+ expect(page.mmv_metadata_credit_element.when_present).to be_visible
+ expect(page.mmv_metadata_source_element.when_present.text).to match 'Own work'
+
+ # Image metadata
+ expect(page.mmv_image_metadata_wrapper_element.when_present).to be_visible
+ # Description
+ expect(page.mmv_image_metadata_desc_element.when_present.text).to match 'Sunrise over fishing boats on the beach south of Kovalam'
+ # Image metadata links
+ expect(page.mmv_image_metadata_links_wrapper_element.when_present).to be_visible
+ # Details link
+ expect(page.mmv_details_page_link_element.when_present.text).to match 'More details'
+ expect(page.mmv_details_page_link_element.when_present.attribute('href')).to match /boats_in_Kerala.jpg$/
+end
+
+# Helper function that verifies the presence of various elements in viewer
+# while looking at image2 (Aquarium)
+def check_elements_in_viewer_for_image2(page)
+ # Check basic MMV elements are present
+ expect(page.mmv_overlay_element.when_present).to be_visible
+ expect(page.mmv_wrapper_element.when_present).to be_visible
+ expect(page.mmv_image_div_element).to be_visible
+
+ # Check image content
+ expect(page.mmv_final_image_element.when_present(30).attribute('src')).to match 'Offsite'
+
+ # Check basic metadata is present
+
+ # Title
+ expect(page.mmv_metadata_title_element.when_present.text).to match /^Tropical Fish Aquarium$/
+ # License
+ expect(page.mmv_metadata_license_element.when_present(10).attribute('href')).to match %r{^https?://creativecommons.org/licenses/by-sa/3.0$}
+ expect(page.mmv_metadata_license_element.when_present.text).to match 'CC BY-SA 3.0'
+ # Credit
+ expect(page.mmv_metadata_credit_element.when_present).to be_visible
+ expect(page.mmv_metadata_source_element.when_present.text).to match 'Wikimedia Foundation'
+
+ # Image metadata
+ expect(page.mmv_image_metadata_wrapper_element.when_present).to be_visible
+ # Description
+ expect(page.mmv_image_metadata_desc_element.when_present.text).to match 'Photo from Wikimedia Foundation'
+ # Image metadata links
+ expect(page.mmv_image_metadata_links_wrapper_element.when_present).to be_visible
+ # Details link
+ expect(page.mmv_details_page_link_element.when_present.text).to match 'More details'
+ expect(page.mmv_details_page_link_element.when_present.attribute('href')).to match /All_Hands_Offsite.*\.jpg$/
+end
+
+# Helper function that verifies the presence of various elements in viewer
+# while looking at image3 (Hong Kong)
+def check_elements_in_viewer_for_image3(page)
+ # Check basic MMV elements are present
+ expect(page.mmv_overlay_element.when_present).to be_visible
+ expect(page.mmv_wrapper_element.when_present).to be_visible
+ expect(page.mmv_image_div_element).to be_visible
+
+ # Check image content
+ expect(page.mmv_image_div_element.image_element.attribute('src')).to match 'Hong_Kong'
+
+ # Check basic metadata is present
+
+ # Title
+ expect(page.mmv_metadata_title_element.when_present.text).to match /^Hong Kong Harbor at night$/
+ # License
+ expect(page.mmv_metadata_license_element.when_present.attribute('href')).to match %r{^https?://creativecommons.org/licenses/by-sa/3.0$}
+ expect(page.mmv_metadata_license_element.when_present.text).to match 'CC BY-SA 3.0'
+ # Credit
+ expect(page.mmv_metadata_credit_element.when_present).to be_visible
+ expect(page.mmv_metadata_source_element.when_present.text).to match 'Wikimedia Foundation'
+
+ # Image metadata
+ expect(page.mmv_image_metadata_wrapper_element.when_present).to be_visible
+ # Description
+ expect(page.mmv_image_metadata_desc_element.when_present.text).to match /Photos from our product team's talks at Wikimania 2013 in Hong Kong./
+ # Image metadata links
+ expect(page.mmv_image_metadata_links_wrapper_element.when_present).to be_visible
+ # Details link
+ expect(page.mmv_details_page_link_element.when_present.text).to match 'More details'
+ expect(page.mmv_details_page_link_element.when_present.attribute('href')).to match /Wikimania_2013_-_Hong_Kong_-_Photo_090\.jpg$/
+end
diff --git a/www/wiki/extensions/MultimediaViewer/tests/browser/features/support/env.rb b/www/wiki/extensions/MultimediaViewer/tests/browser/features/support/env.rb
new file mode 100644
index 00000000..c1072b26
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/browser/features/support/env.rb
@@ -0,0 +1,3 @@
+require 'mediawiki_selenium/cucumber'
+require 'mediawiki_selenium/pages'
+require 'mediawiki_selenium/step_definitions'
diff --git a/www/wiki/extensions/MultimediaViewer/tests/browser/features/support/pages/commons_page.rb b/www/wiki/extensions/MultimediaViewer/tests/browser/features/support/pages/commons_page.rb
new file mode 100644
index 00000000..0923e354
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/browser/features/support/pages/commons_page.rb
@@ -0,0 +1,51 @@
+require 'json'
+
+class CommonsPage
+ include PageObject
+
+ page_url 'File:Sunrise_over_fishing_boats_in_Kerala.jpg'
+
+ img(:commons_image, src: /Kerala\.jpg$/)
+ div(:mmv_image_loaded_cucumber, class: 'mw-mmv-image-loaded-cucumber')
+
+ def wait_for_image_load(selector)
+ browser.execute_script <<-end_script
+ function wait_for_image() {
+ var $img = $( #{selector.to_json} );
+ if ( $img.length
+ && $img.attr( 'src' ).match(/Kerala/)
+ && !$img.attr( 'src' ).match(/\\/220px-/) // Blurry placeholder
+ && $img.prop( 'complete' ) ) {
+ $( 'body' ).append( '<div class=\"mw-mmv-image-loaded-cucumber\"/>' );
+ } else {
+ setTimeout( wait_for_image, 10 );
+ }
+ }
+ wait_for_image();
+ end_script
+
+ wait_until { mmv_image_loaded_cucumber_element.exists? }
+ end
+
+ def log_performance(stats)
+ stats = stats.reject { |_name, value| value.nil? || value.to_s.empty? }
+ stats[:duration] = stats[:duration].floor
+
+ browser.execute_script <<-end_script
+ mediaWiki.eventLog.declareSchema( 'MultimediaViewerVersusPageFilePerformance',
+ { schema:
+ { title: 'MultimediaViewerVersusPageFilePerformance',
+ properties: {
+ type: { type: 'string', required: true, enum: [ 'mmv', 'file-page' ] },
+ duration: { type: 'integer', required: true },
+ cache: { type: 'string', required: false, enum: [ 'cold', 'warm' ] },
+ windowSize: { type: 'string', required: false, enum: [ 'average', 'large'] }
+ }
+ },
+ revision: 7907636
+ });
+
+ mw.eventLog.logEvent( 'MultimediaViewerVersusPageFilePerformance', #{stats.to_json} );
+ end_script
+ end
+end
diff --git a/www/wiki/extensions/MultimediaViewer/tests/browser/features/support/pages/e2e_test_page.rb b/www/wiki/extensions/MultimediaViewer/tests/browser/features/support/pages/e2e_test_page.rb
new file mode 100644
index 00000000..c0f04077
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/browser/features/support/pages/e2e_test_page.rb
@@ -0,0 +1,87 @@
+class E2ETestPage < CommonsPage
+ include PageObject
+
+ page_url 'MediaViewerE2ETest'
+
+ # Tag page elements that we will need.
+
+ # First image in lightbox demo page
+ a(:image1_in_article, class: 'image', href: /Kerala\.jpg$/)
+ a(:image2_in_article, class: 'image', href: /Wikimedia_Foundation_2013_All_Hands_Offsite_-_Day_2_-_Photo_24\.jpg$/)
+
+ a(:other_image_in_article, href: /Academy_of_Sciences\.jpg$/)
+
+ # Black overlay
+ div(:mmv_overlay, class: 'mw-mmv-overlay')
+
+ # Wrapper div for all mmv elements
+ div(:mmv_wrapper, class: 'mw-mmv-wrapper')
+
+ # Wrapper div for image
+ div(:mmv_image_div, class: 'mw-mmv-image')
+
+ # Actual image
+ image(:mmv_final_image, class: 'mw-mmv-final-image')
+
+ # Metadata elements
+ span(:mmv_metadata_title, class: 'mw-mmv-title')
+ a(:mmv_metadata_license, class: 'mw-mmv-license')
+ p(:mmv_metadata_credit, class: 'mw-mmv-credit')
+ span(:mmv_metadata_source, class: 'mw-mmv-source')
+
+ div(:mmv_image_metadata_wrapper, class: 'mw-mmv-image-metadata')
+ p(:mmv_image_metadata_desc, class: 'mw-mmv-image-desc')
+
+ ul(:mmv_image_metadata_links_wrapper, class: 'mw-mmv-image-links')
+ a(:mmv_details_page_link, class: 'mw-mmv-description-page-button')
+
+ # Controls
+ button(:mmv_next_button, class: 'mw-mmv-next-image')
+ button(:mmv_previous_button, class: 'mw-mmv-prev-image')
+ button(:mmv_close_button, class: 'mw-mmv-close')
+ div(:mmv_image_loaded_cucumber, class: 'mw-mmv-image-loaded-cucumber')
+
+ # Download
+ button(:mmv_download_icon, class: 'mw-mmv-download-button')
+ div(:mmv_download_menu, class: 'mw-mmv-download-dialog')
+ span(:mmv_download_size_label, class: 'mw-mmv-download-image-size')
+ span(:mmv_download_down_arrow_icon, class: 'mw-mmv-download-select-menu')
+ div(:mmv_download_size_menu_container, class: 'mw-mmv-download-size')
+ div(:mmv_download_size_menu) do |page|
+ page.mmv_download_size_menu_container_element.div_element(class: 'oo-ui-selectWidget')
+ end
+ divs(:mmv_download_size_options, class: 'oo-ui-menuOptionWidget')
+ a(:mmv_download_link, class: 'mw-mmv-download-go-button')
+ a(:mmv_download_preview_link, class: 'mw-mmv-download-preview-link')
+ div(:mmv_download_attribution_area, class: 'mw-mmv-download-attribution')
+ p(:mmv_download_attribution_area_close_icon, class: 'mw-mmv-download-attribution-close-button')
+ div(:mmv_download_attribution_area_input_container, class: 'mw-mmv-download-attr-input')
+ text_field(:mmv_download_attribution_area_input) do |page|
+ page.mmv_download_attribution_area_input_container_element.text_field_element
+ end
+
+ # Options
+ button(:mmv_options_icon, class: 'mw-mmv-options-button')
+ div(:mmv_options_menu_disable, class: 'mw-mmv-options-disable')
+ div(:mmv_options_menu_enable, class: 'mw-mmv-options-enable')
+ button(:mmv_options_enable_button) do |page|
+ page.mmv_options_menu_enable_element.div_element(class: 'mw-mmv-options-submit').button_element(class: 'mw-mmv-options-submit-button')
+ end
+ button(:mmv_options_disable_button) do |page|
+ page.mmv_options_menu_disable_element.div_element(class: 'mw-mmv-options-submit').button_element(class: 'mw-mmv-options-submit-button')
+ end
+ button(:mmv_options_enable_cancel_button) do |page|
+ page.mmv_options_menu_enable_element.div_element(class: 'mw-mmv-options-submit').button_element(class: 'mw-mmv-options-cancel-button')
+ end
+ button(:mmv_options_disable_cancel_button) do |page|
+ page.mmv_options_menu_disable_element.div_element(class: 'mw-mmv-options-submit').button_element(class: 'mw-mmv-options-cancel-button')
+ end
+ div(:mmv_options_disable_confirmation, class: 'mw-mmv-disable-confirmation')
+ div(:mmv_options_disable_x_icon) do |page|
+ page.mmv_options_disable_confirmation_element.div_element(class: 'mw-mmv-confirmation-close')
+ end
+ div(:mmv_options_enable_confirmation, class: 'mw-mmv-enable-confirmation')
+ div(:mmv_options_enable_x_icon) do |page|
+ page.mmv_options_enable_confirmation_element.div_element(class: 'mw-mmv-confirmation-close')
+ end
+end
diff --git a/www/wiki/extensions/MultimediaViewer/tests/browser/samples/MediaViewerE2ETest.wikitext b/www/wiki/extensions/MultimediaViewer/tests/browser/samples/MediaViewerE2ETest.wikitext
new file mode 100644
index 00000000..a920e77f
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/browser/samples/MediaViewerE2ETest.wikitext
@@ -0,0 +1,14 @@
+<big>PLEASE DO NOT EDIT THIS PAGE! IT NEEDS TO STAY THE SAME FOR THE PURPOSE OF AUTOMATED TESTING</big>
+
+==Test Images==
+Here are some sample images for testing different features of Media Viewer.
+
+[[File:Sunrise over fishing boats in Kerala.jpg|thumb|left|Sunrise over fishing boats]] [[File:Wikimedia_Foundation_2013_All_Hands_Offsite_-_Day_2_-_Photo_24.jpg|thumb|Tropical Fish Aquarium]] [[File:Wikimania 2013 - Hong Kong - Photo 090.jpg|thumb|center|Hong Kong Harbor at night]]
+<br clear="all"/>
+
+[[File:Wikimedia_Foundation_2013_All_Hands_Offsite_-_Day_2_-_Photo_16.jpg|thumb|left|Nautilus Shell at California Academy of Sciences]] [[File:Multimedia_Team_-_Wikimedia_Foundation.jpg|thumb|center|Multimedia Team]] [[File:Zonotrichia atricapilla -British Columbia, Canada-8.jpg|thumb|Golden-crowned Sparrow]]
+<br clear="all"/>
+
+[[File:Multimedia Roundtable 5 Photo 2.jpg|thumb|left|Multimedia Roundtable]] [[File:Wikimedia Foundation - Team 1 - California Academy of Sciences.jpg|thumb|Wikimedia Team]]
+
+<br clear="all"/>
diff --git a/www/wiki/extensions/MultimediaViewer/tests/phan/config.php b/www/wiki/extensions/MultimediaViewer/tests/phan/config.php
new file mode 100644
index 00000000..0cfe0c1a
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/phan/config.php
@@ -0,0 +1,19 @@
+<?php
+
+$cfg = require __DIR__ . '/../../vendor/mediawiki/mediawiki-phan-config/src/config.php';
+
+$cfg['directory_list'] = array_merge(
+ $cfg['directory_list'],
+ [
+ './../../extensions/BetaFeatures',
+ ]
+);
+
+$cfg['exclude_analysis_directory_list'] = array_merge(
+ $cfg['exclude_analysis_directory_list'],
+ [
+ './../../extensions/BetaFeatures',
+ ]
+);
+
+return $cfg;
diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/logging/mmv.logging.ActionLogger.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/logging/mmv.logging.ActionLogger.test.js
new file mode 100644
index 00000000..9a1808c4
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/logging/mmv.logging.ActionLogger.test.js
@@ -0,0 +1,48 @@
+( function ( mw, $ ) {
+ QUnit.module( 'mmv.logging.ActionLogger', QUnit.newMwEnvironment() );
+
+ QUnit.test( 'log()', function ( assert ) {
+ var fakeEventLog = { logEvent: this.sandbox.stub() },
+ logger = new mw.mmv.logging.ActionLogger(),
+ action1key = 'test-1',
+ action1value = 'Test',
+ action2key = 'test-2',
+ action2value = 'Foo $1 $2 bar',
+ unknownAction = 'test-3',
+ clock = this.sandbox.useFakeTimers();
+
+ this.sandbox.stub( logger, 'loadDependencies' ).returns( $.Deferred().resolve() );
+ this.sandbox.stub( mw, 'log' );
+
+ logger.samplingFactorMap = { 'default': 1 };
+ logger.setEventLog( fakeEventLog );
+ logger.logActions = {};
+ logger.logActions[ action1key ] = action1value;
+ logger.logActions[ action2key ] = action2value;
+
+ logger.log( unknownAction );
+ clock.tick( 10 );
+
+ assert.strictEqual( mw.log.lastCall.args[ 0 ], unknownAction, 'Log message defaults to unknown key' );
+ assert.ok( fakeEventLog.logEvent.called, 'event log has been recorded' );
+
+ mw.log.reset();
+ fakeEventLog.logEvent.reset();
+ logger.log( action1key );
+ clock.tick( 10 );
+
+ assert.strictEqual( mw.log.lastCall.args[ 0 ], action1value, 'Log message is translated to its text' );
+ assert.ok( fakeEventLog.logEvent.called, 'event log has been recorded' );
+
+ mw.log.reset();
+ fakeEventLog.logEvent.reset();
+ logger.samplingFactorMap = { 'default': 0 };
+ logger.log( action1key, true );
+ clock.tick( 10 );
+
+ assert.ok( !mw.log.called, 'No logging when disabled' );
+ assert.ok( fakeEventLog.logEvent.called, 'event log has been recorded' );
+
+ clock.restore();
+ } );
+}( mediaWiki, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/logging/mmv.logging.AttributionLogger.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/logging/mmv.logging.AttributionLogger.test.js
new file mode 100644
index 00000000..e62d8109
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/logging/mmv.logging.AttributionLogger.test.js
@@ -0,0 +1,22 @@
+( function ( mw, $ ) {
+ QUnit.module( 'mmv.logging.AttributionLogger', QUnit.newMwEnvironment() );
+
+ QUnit.test( 'log()', function ( assert ) {
+ var fakeEventLog = { logEvent: this.sandbox.stub() },
+ logger = new mw.mmv.logging.AttributionLogger(),
+ image = { author: 'foo', source: 'bar', license: {} },
+ emptyImage = {};
+
+ this.sandbox.stub( logger, 'loadDependencies' ).returns( $.Deferred().resolve() );
+ this.sandbox.stub( mw, 'log' );
+
+ logger.samplingFactor = 1;
+ logger.setEventLog( fakeEventLog );
+
+ logger.logAttribution( image );
+ assert.ok( true, 'logDimensions() did not throw errors' );
+
+ logger.logAttribution( emptyImage );
+ assert.ok( true, 'logDimensions() did not throw errors for empty image' );
+ } );
+}( mediaWiki, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/logging/mmv.logging.DimensionLogger.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/logging/mmv.logging.DimensionLogger.test.js
new file mode 100644
index 00000000..0df8c02f
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/logging/mmv.logging.DimensionLogger.test.js
@@ -0,0 +1,17 @@
+( function ( mw, $ ) {
+ QUnit.module( 'mmv.logging.DimensionLogger', QUnit.newMwEnvironment() );
+
+ QUnit.test( 'log()', function ( assert ) {
+ var fakeEventLog = { logEvent: this.sandbox.stub() },
+ logger = new mw.mmv.logging.DimensionLogger();
+
+ this.sandbox.stub( logger, 'loadDependencies' ).returns( $.Deferred().resolve() );
+ this.sandbox.stub( mw, 'log' );
+
+ logger.samplingFactor = 1;
+ logger.setEventLog( fakeEventLog );
+
+ logger.logDimensions( 640, 480, 200, 'resize' );
+ assert.ok( true, 'logDimensions() did not throw errors' );
+ } );
+}( mediaWiki, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/logging/mmv.logging.DurationLogger.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/logging/mmv.logging.DurationLogger.test.js
new file mode 100644
index 00000000..474fc08c
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/logging/mmv.logging.DurationLogger.test.js
@@ -0,0 +1,218 @@
+( function ( mw, $ ) {
+ QUnit.module( 'mmv.logging.DurationLogger', QUnit.newMwEnvironment( {
+ setup: function () {
+ this.clock = this.sandbox.useFakeTimers();
+
+ // since jQuery 2/3, $.now will capture a reference to Date.now
+ // before above fake timer gets a chance to override it, so I'll
+ // override that new behavior in order to run these tests...
+ // @see https://github.com/sinonjs/lolex/issues/76
+ this.oldNow = $.now;
+ $.now = function () { return +( new Date() ); };
+ },
+
+ teardown: function () {
+ $.now = this.oldNow;
+ this.clock.restore();
+ }
+ } ) );
+
+ QUnit.test( 'start()', function ( assert ) {
+ var durationLogger = new mw.mmv.durationLogger.constructor();
+ durationLogger.samplingFactor = 1;
+
+ try {
+ durationLogger.start();
+ } catch ( e ) {
+ assert.ok( true, 'Exception raised when calling start() without parameters' );
+ }
+ assert.ok( $.isEmptyObject( durationLogger.starts ), 'No events saved by DurationLogger' );
+
+ durationLogger.start( 'foo' );
+ assert.strictEqual( durationLogger.starts.foo, 0, 'Event start saved' );
+
+ this.clock.tick( 1000 );
+ durationLogger.start( 'bar' );
+ assert.strictEqual( durationLogger.starts.bar, 1000, 'Later event start saved' );
+
+ durationLogger.start( 'foo' );
+ assert.strictEqual( durationLogger.starts.foo, 0, 'Event start not overritten' );
+
+ this.clock.tick( 666 );
+ durationLogger.start( [ 'baz', 'bob', 'bar' ] );
+ assert.strictEqual( durationLogger.starts.baz, 1666, 'First simultaneous event start saved' );
+ assert.strictEqual( durationLogger.starts.bob, 1666, 'Second simultaneous event start saved' );
+ assert.strictEqual( durationLogger.starts.bar, 1000, 'Third simultaneous event start not overwritten' );
+ } );
+
+ QUnit.test( 'stop()', function ( assert ) {
+ var durationLogger = new mw.mmv.durationLogger.constructor();
+
+ try {
+ durationLogger.stop();
+ } catch ( e ) {
+ assert.ok( true, 'Exception raised when calling stop() without parameters' );
+ }
+
+ durationLogger.stop( 'foo' );
+
+ assert.strictEqual( durationLogger.stops.foo, 0, 'Event stop saved' );
+
+ this.clock.tick( 1000 );
+ durationLogger.stop( 'foo' );
+
+ assert.strictEqual( durationLogger.stops.foo, 0, 'Event stop not overwritten' );
+
+ durationLogger.stop( 'foo', 1 );
+
+ assert.strictEqual( durationLogger.starts.foo, 1, 'Event start saved' );
+
+ durationLogger.stop( 'foo', 2 );
+
+ assert.strictEqual( durationLogger.starts.foo, 1, 'Event start not overwritten' );
+ } );
+
+ QUnit.test( 'record()', function ( assert ) {
+ var dependenciesDeferred = $.Deferred(),
+ fakeEventLog = { logEvent: this.sandbox.stub() },
+ durationLogger = new mw.mmv.durationLogger.constructor();
+
+ durationLogger.samplingFactor = 1;
+ durationLogger.schemaSupportsCountry = this.sandbox.stub().returns( true );
+
+ this.sandbox.stub( mw.user, 'isAnon' ).returns( false );
+ this.sandbox.stub( durationLogger, 'loadDependencies' ).returns( dependenciesDeferred.promise() );
+
+ try {
+ durationLogger.record();
+ } catch ( e ) {
+ assert.ok( true, 'Exception raised when calling record() without parameters' );
+ }
+
+ durationLogger.setEventLog( fakeEventLog );
+
+ durationLogger.start( 'bar' );
+ this.clock.tick( 1000 );
+ durationLogger.stop( 'bar' );
+ durationLogger.record( 'bar' );
+
+ assert.ok( !fakeEventLog.logEvent.called, 'Event queued if dependencies not loaded' );
+
+ // Queue a second item
+
+ durationLogger.start( 'bob' );
+ this.clock.tick( 4000 );
+ durationLogger.stop( 'bob' );
+ durationLogger.record( 'bob' );
+
+ assert.ok( !fakeEventLog.logEvent.called, 'Event queued if dependencies not loaded' );
+
+ dependenciesDeferred.resolve();
+ this.clock.tick( 10 );
+
+ assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 0 ], 'MultimediaViewerDuration', 'EventLogging schema is correct' );
+ assert.deepEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ], { type: 'bar', duration: 1000, loggedIn: true, samplingFactor: 1 },
+ 'EventLogging data is correct' );
+
+ assert.strictEqual( fakeEventLog.logEvent.getCall( 1 ).args[ 0 ], 'MultimediaViewerDuration', 'EventLogging schema is correct' );
+ assert.deepEqual( fakeEventLog.logEvent.getCall( 1 ).args[ 1 ], { type: 'bob', duration: 4000, loggedIn: true, samplingFactor: 1 },
+ 'EventLogging data is correct' );
+
+ assert.strictEqual( fakeEventLog.logEvent.callCount, 2, 'logEvent called when processing the queue' );
+
+ durationLogger.start( 'foo' );
+ this.clock.tick( 3000 );
+ durationLogger.stop( 'foo' );
+ durationLogger.record( 'foo' );
+ this.clock.tick( 10 );
+
+ assert.strictEqual( fakeEventLog.logEvent.getCall( 2 ).args[ 0 ], 'MultimediaViewerDuration', 'EventLogging schema is correct' );
+ assert.deepEqual( fakeEventLog.logEvent.getCall( 2 ).args[ 1 ], { type: 'foo', duration: 3000, loggedIn: true, samplingFactor: 1 },
+ 'EventLogging data is correct' );
+
+ assert.strictEqual( durationLogger.starts.bar, undefined, 'Start value deleted after record' );
+ assert.strictEqual( durationLogger.stops.bar, undefined, 'Stop value deleted after record' );
+
+ durationLogger.setGeo( { country: 'FR' } );
+ mw.user.isAnon.returns( true );
+
+ durationLogger.start( 'baz' );
+ this.clock.tick( 2000 );
+ durationLogger.stop( 'baz' );
+ durationLogger.record( 'baz' );
+ this.clock.tick( 10 );
+
+ assert.strictEqual( fakeEventLog.logEvent.getCall( 3 ).args[ 0 ], 'MultimediaViewerDuration', 'EventLogging schema is correct' );
+ assert.deepEqual( fakeEventLog.logEvent.getCall( 3 ).args[ 1 ], { type: 'baz', duration: 2000, loggedIn: false, country: 'FR', samplingFactor: 1 },
+ 'EventLogging data is correct' );
+
+ assert.strictEqual( durationLogger.starts.bar, undefined, 'Start value deleted after record' );
+ assert.strictEqual( durationLogger.stops.bar, undefined, 'Stop value deleted after record' );
+
+ durationLogger.stop( 'fooz', $.now() - 9000 );
+ durationLogger.record( 'fooz' );
+ this.clock.tick( 10 );
+
+ assert.deepEqual( fakeEventLog.logEvent.getCall( 4 ).args[ 1 ], { type: 'fooz', duration: 9000, loggedIn: false, country: 'FR', samplingFactor: 1 },
+ 'EventLogging data is correct' );
+
+ assert.strictEqual( fakeEventLog.logEvent.callCount, 5, 'logEvent has been called fives times at this point in the test' );
+
+ durationLogger.stop( 'foo' );
+ durationLogger.record( 'foo' );
+ this.clock.tick( 10 );
+
+ assert.strictEqual( fakeEventLog.logEvent.callCount, 5, 'Record without a start doesn\'t get logged' );
+
+ durationLogger.start( 'foofoo' );
+ durationLogger.record( 'foofoo' );
+ this.clock.tick( 10 );
+
+ assert.strictEqual( fakeEventLog.logEvent.callCount, 5, 'Record without a stop doesn\'t get logged' );
+
+ durationLogger.start( 'extra' );
+ this.clock.tick( 5000 );
+ durationLogger.stop( 'extra' );
+ durationLogger.record( 'extra', { bim: 'bam' } );
+ this.clock.tick( 10 );
+
+ assert.deepEqual( fakeEventLog.logEvent.getCall( 5 ).args[ 1 ], { type: 'extra', duration: 5000, loggedIn: false, country: 'FR', samplingFactor: 1, bim: 'bam' },
+ 'EventLogging data is correct' );
+ } );
+
+ QUnit.test( 'loadDependencies()', function ( assert ) {
+ var promise,
+ durationLogger = new mw.mmv.durationLogger.constructor();
+
+ this.sandbox.stub( mw.loader, 'using' );
+
+ mw.loader.using.withArgs( [ 'ext.eventLogging', 'schema.MultimediaViewerDuration' ] ).throwsException( 'EventLogging is missing' );
+
+ promise = durationLogger.loadDependencies();
+ this.clock.tick( 10 );
+
+ assert.strictEqual( promise.state(), 'rejected', 'Promise is rejected' );
+
+ // It's necessary to reset the stub, otherwise the original withArgs keeps running alongside the new one
+ mw.loader.using.restore();
+ this.sandbox.stub( mw.loader, 'using' );
+
+ mw.loader.using.withArgs( [ 'ext.eventLogging', 'schema.MultimediaViewerDuration' ] ).throwsException( 'EventLogging is missing' );
+
+ promise = durationLogger.loadDependencies();
+ this.clock.tick( 10 );
+
+ assert.strictEqual( promise.state(), 'rejected', 'Promise is rejected' );
+
+ // It's necessary to reset the stub, otherwise the original withArgs keeps running alongside the new one
+ mw.loader.using.restore();
+ this.sandbox.stub( mw.loader, 'using' );
+
+ mw.loader.using.withArgs( [ 'ext.eventLogging', 'schema.MultimediaViewerDuration' ] ).callsArg( 1 );
+
+ promise = durationLogger.loadDependencies();
+ this.clock.tick( 10 );
+
+ assert.strictEqual( promise.state(), 'resolved', 'Promise is resolved' );
+ } );
+}( mediaWiki, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/logging/mmv.logging.PerformanceLogger.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/logging/mmv.logging.PerformanceLogger.test.js
new file mode 100644
index 00000000..81a621f7
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/logging/mmv.logging.PerformanceLogger.test.js
@@ -0,0 +1,341 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $ ) {
+ QUnit.module( 'mmv.logging.PerformanceLogger', QUnit.newMwEnvironment() );
+
+ function createFakeXHR( response ) {
+ return {
+ readyState: 0,
+ open: $.noop,
+ send: function () {
+ var xhr = this;
+
+ setTimeout( function () {
+ xhr.readyState = 4;
+ xhr.response = response;
+ if ( $.isFunction( xhr.onreadystatechange ) ) {
+ xhr.onreadystatechange();
+ }
+ }, 0 );
+ }
+ };
+ }
+
+ QUnit.test( 'recordEntry: basic', function ( assert ) {
+ var performance = new mw.mmv.logging.PerformanceLogger(),
+ fakeEventLog = { logEvent: this.sandbox.stub() },
+ type = 'gender',
+ total = 100,
+ // we'll be waiting for 4 promises to complete
+ asyncs = [ assert.async(), assert.async(), assert.async(), assert.async() ];
+
+ this.sandbox.stub( performance, 'loadDependencies' ).returns( $.Deferred().resolve() );
+ this.sandbox.stub( performance, 'isInSample' );
+ performance.setEventLog( fakeEventLog );
+
+ performance.isInSample.returns( false );
+
+ performance.recordEntry( type, total ).then( null, function () {
+ assert.strictEqual( fakeEventLog.logEvent.callCount, 0, 'No stats should be logged if not in sample' );
+ asyncs.pop()();
+ } );
+
+ performance.isInSample.returns( true );
+
+ performance.recordEntry( type, total ).then( null, function () {
+ assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 0 ], 'MultimediaViewerNetworkPerformance', 'EventLogging schema is correct' );
+ assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].type, type, 'type is correct' );
+ assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].total, total, 'total is correct' );
+ assert.strictEqual( fakeEventLog.logEvent.callCount, 1, 'Stats should be logged' );
+ asyncs.pop()();
+ } );
+
+ performance.recordEntry( type, total, 'URL' ).then( null, function () {
+ assert.strictEqual( fakeEventLog.logEvent.callCount, 2, 'Stats should be logged' );
+ asyncs.pop()();
+ } );
+
+ performance.recordEntry( type, total, 'URL' ).then( null, function () {
+ assert.strictEqual( fakeEventLog.logEvent.callCount, 2, 'Stats should not be logged a second time for the same URL' );
+ asyncs.pop()();
+ } );
+ } );
+
+ QUnit.test( 'recordEntry: with Navigation Timing data', function ( assert ) {
+ var fakeRequest,
+ varnish1 = 'cp1061',
+ varnish2 = 'cp3006',
+ varnish3 = 'cp3005',
+ varnish1hits = 0,
+ varnish2hits = 2,
+ varnish3hits = 1,
+ xvarnish = '1754811951 1283049064, 1511828531, 1511828573 1511828528',
+ xcache = varnish1 + ' miss (0), ' + varnish2 + ' miss (2), ' + varnish3 + ' frontend hit (1), malformed(5)',
+ age = '12345',
+ contentLength = '23456',
+ urlHost = 'fail',
+ date = 'Tue, 04 Feb 2014 11:11:50 GMT',
+ timestamp = 1391512310,
+ url = 'https://' + urlHost + '/balls.jpg',
+ redirect = 500,
+ dns = 2,
+ tcp = 10,
+ request = 25,
+ response = 50,
+ cache = 1,
+ perfData = {
+ initiatorType: 'xmlhttprequest',
+ name: url,
+ duration: 12345,
+ redirectStart: 1000,
+ redirectEnd: 1500,
+ domainLookupStart: 2,
+ domainLookupEnd: 4,
+ connectStart: 50,
+ connectEnd: 60,
+ requestStart: 125,
+ responseStart: 150,
+ responseEnd: 200,
+ fetchStart: 1
+ },
+ country = 'FR',
+ type = 'image',
+ performance = new mw.mmv.logging.PerformanceLogger(),
+ status = 200,
+ metered = true,
+ bandwidth = 45.67,
+ fakeEventLog = { logEvent: this.sandbox.stub() },
+ done = assert.async();
+
+ this.sandbox.stub( performance, 'loadDependencies' ).returns( $.Deferred().resolve() );
+ performance.setEventLog( fakeEventLog );
+ performance.schemaSupportsCountry = this.sandbox.stub().returns( true );
+
+ this.sandbox.stub( performance, 'getWindowPerformance' ).returns( {
+ getEntriesByName: function () {
+ return [ perfData, {
+ initiatorType: 'bogus',
+ duration: 1234,
+ name: url
+ } ];
+ }
+ } );
+
+ this.sandbox.stub( performance, 'getNavigatorConnection' ).returns( { metered: metered, bandwidth: bandwidth } );
+ this.sandbox.stub( performance, 'isInSample' ).returns( true );
+
+ fakeRequest = {
+ getResponseHeader: function ( header ) {
+ switch ( header ) {
+ case 'X-Cache':
+ return xcache;
+ case 'X-Varnish':
+ return xvarnish;
+ case 'Age':
+ return age;
+ case 'Content-Length':
+ return contentLength;
+ case 'Date':
+ return date;
+ }
+ },
+ status: status
+ };
+
+ performance.setGeo( { country: country } );
+
+ performance.recordEntry( type, 100, url, fakeRequest ).then( null, function () {
+ assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 0 ], 'MultimediaViewerNetworkPerformance', 'EventLogging schema is correct' );
+ assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].type, type, 'type is correct' );
+ assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].varnish1, varnish1, 'varnish1 is correct' );
+ assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].varnish2, varnish2, 'varnish2 is correct' );
+ assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].varnish3, varnish3, 'varnish3 is correct' );
+ assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].varnish4, undefined, 'varnish4 is undefined' );
+ assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].varnish1hits, varnish1hits, 'varnish1hits is correct' );
+ assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].varnish2hits, varnish2hits, 'varnish2hits is correct' );
+ assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].varnish3hits, varnish3hits, 'varnish3hits is correct' );
+ assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].varnish4hits, undefined, 'varnish4hits is undefined' );
+ assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].XVarnish, xvarnish, 'XVarnish is correct' );
+ assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].XCache, xcache, 'XCache is correct' );
+ assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].age, parseInt( age, 10 ), 'age is correct' );
+ assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].contentLength, parseInt( contentLength, 10 ), 'contentLength is correct' );
+ assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].contentHost, window.location.host, 'contentHost is correct' );
+ assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].urlHost, urlHost, 'urlHost is correct' );
+ assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].timestamp, timestamp, 'timestamp is correct' );
+ assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].total, perfData.duration, 'total is correct' );
+ assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].redirect, redirect, 'redirect is correct' );
+ assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].dns, dns, 'dns is correct' );
+ assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].tcp, tcp, 'tcp is correct' );
+ assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].request, request, 'request is correct' );
+ assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].response, response, 'response is correct' );
+ assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].cache, cache, 'cache is correct' );
+ assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].country, country, 'country is correct' );
+ assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].isHttps, true, 'isHttps is correct' );
+ assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].status, status, 'status is correct' );
+ assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].metered, metered, 'metered is correct' );
+ assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].bandwidth, Math.round( bandwidth ), 'bandwidth is correct' );
+ done();
+ } );
+ } );
+
+ QUnit.test( 'recordEntry: with async extra stats', function ( assert ) {
+ var performance = new mw.mmv.logging.PerformanceLogger(),
+ fakeEventLog = { logEvent: this.sandbox.stub() },
+ type = 'gender',
+ total = 100,
+ overriddenType = 'image',
+ foo = 'bar',
+ extraStatsPromise = $.Deferred(),
+ clock = this.sandbox.useFakeTimers();
+
+ this.sandbox.stub( performance, 'loadDependencies' ).returns( $.Deferred().resolve() );
+ this.sandbox.stub( performance, 'isInSample' );
+ performance.setEventLog( fakeEventLog );
+
+ performance.isInSample.returns( true );
+
+ performance.recordEntry( type, total, 'URL1', undefined, extraStatsPromise );
+
+ assert.strictEqual( fakeEventLog.logEvent.callCount, 0, 'Stats should not be logged if the promise hasn\'t completed yet' );
+
+ extraStatsPromise.reject();
+
+ extraStatsPromise.then( null, function () {
+ assert.strictEqual( fakeEventLog.logEvent.callCount, 1, 'Stats should be logged' );
+
+ assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 0 ], 'MultimediaViewerNetworkPerformance', 'EventLogging schema is correct' );
+ assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].type, type, 'type is correct' );
+ assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].total, total, 'total is correct' );
+ } );
+
+ // make sure first promise is completed before recording another entry,
+ // to make sure data in fakeEventLog doesn't suffer race conditions
+ clock.tick( 10 );
+ clock.restore();
+
+ extraStatsPromise = $.Deferred();
+
+ performance.recordEntry( type, total, 'URL2', undefined, extraStatsPromise );
+
+ assert.strictEqual( fakeEventLog.logEvent.callCount, 1, 'Stats should not be logged if the promise hasn\'t been resolved yet' );
+
+ extraStatsPromise.resolve( { type: overriddenType, foo: foo } );
+
+ return extraStatsPromise.then( function () {
+ assert.strictEqual( fakeEventLog.logEvent.callCount, 2, 'Stats should be logged' );
+
+ assert.strictEqual( fakeEventLog.logEvent.getCall( 1 ).args[ 0 ], 'MultimediaViewerNetworkPerformance', 'EventLogging schema is correct' );
+ assert.strictEqual( fakeEventLog.logEvent.getCall( 1 ).args[ 1 ].type, overriddenType, 'type is correct' );
+ assert.strictEqual( fakeEventLog.logEvent.getCall( 1 ).args[ 1 ].total, total, 'total is correct' );
+ assert.strictEqual( fakeEventLog.logEvent.getCall( 1 ).args[ 1 ].foo, foo, 'extra stat is correct' );
+ } );
+ } );
+
+ QUnit.test( 'parseVarnishXCacheHeader', function ( assert ) {
+ var varnish1 = 'cp1061',
+ varnish2 = 'cp3006',
+ varnish3 = 'cp3005',
+ testString = varnish1 + ' miss (0), ' + varnish2 + ' miss (0), ' + varnish3 + ' frontend hit (1)',
+ performance = new mw.mmv.logging.PerformanceLogger(),
+ varnishXCache = performance.parseVarnishXCacheHeader( testString );
+
+ assert.strictEqual( varnishXCache.varnish1, varnish1, 'First varnish server name extracted' );
+ assert.strictEqual( varnishXCache.varnish2, varnish2, 'Second varnish server name extracted' );
+ assert.strictEqual( varnishXCache.varnish3, varnish3, 'Third varnish server name extracted' );
+ assert.strictEqual( varnishXCache.varnish4, undefined, 'Fourth varnish server is undefined' );
+ assert.strictEqual( varnishXCache.varnish1hits, 0, 'First varnish hit count extracted' );
+ assert.strictEqual( varnishXCache.varnish2hits, 0, 'Second varnish hit count extracted' );
+ assert.strictEqual( varnishXCache.varnish3hits, 1, 'Third varnish hit count extracted' );
+ assert.strictEqual( varnishXCache.varnish4hits, undefined, 'Fourth varnish hit count is undefined' );
+
+ testString = varnish1 + ' miss (36), ' + varnish2 + ' miss (2)';
+ varnishXCache = performance.parseVarnishXCacheHeader( testString );
+
+ assert.strictEqual( varnishXCache.varnish1, varnish1, 'First varnish server name extracted' );
+ assert.strictEqual( varnishXCache.varnish2, varnish2, 'Second varnish server name extracted' );
+ assert.strictEqual( varnishXCache.varnish3, undefined, 'Third varnish server is undefined' );
+ assert.strictEqual( varnishXCache.varnish1hits, 36, 'First varnish hit count extracted' );
+ assert.strictEqual( varnishXCache.varnish2hits, 2, 'Second varnish hit count extracted' );
+ assert.strictEqual( varnishXCache.varnish3hits, undefined, 'Third varnish hit count is undefined' );
+
+ varnishXCache = performance.parseVarnishXCacheHeader( 'garbage' );
+ assert.ok( $.isEmptyObject( varnishXCache ), 'Varnish cache results are empty' );
+ } );
+
+ QUnit.test( 'record()', function ( assert ) {
+ var type = 'foo',
+ url = 'http://example.com/',
+ response = {},
+ done = assert.async(),
+ performance = new mw.mmv.logging.PerformanceLogger();
+
+ performance.newXHR = function () { return createFakeXHR( response ); };
+
+ performance.recordEntryDelayed = function ( recordType, _, recordUrl, recordRequest ) {
+ assert.strictEqual( recordType, type, 'type is recorded correctly' );
+ assert.strictEqual( recordUrl, url, 'url is recorded correctly' );
+ assert.strictEqual( recordRequest.response, response, 'response is recorded correctly' );
+ done();
+ };
+
+ return performance.record( type, url ).done( function ( recordResponse ) {
+ assert.strictEqual( recordResponse, response, 'response is passed to callback' );
+ } );
+ } );
+
+ QUnit.test( 'record() with old browser', function ( assert ) {
+ var type = 'foo',
+ url = 'http://example.com/',
+ done = assert.async(),
+ performance = new mw.mmv.logging.PerformanceLogger();
+
+ performance.newXHR = function () { throw new Error( 'XMLHttpRequest? What\'s that?' ); };
+
+ performance.record( type, url ).fail( function () {
+ assert.ok( true, 'the promise is rejected when XMLHttpRequest is not supported' );
+ done();
+ } );
+ } );
+
+ QUnit.test( 'mw.mmv.logging.Api', function ( assert ) {
+ var api,
+ oldRecord = mw.mmv.logging.PerformanceLogger.prototype.recordJQueryEntryDelayed,
+ oldAjax = mw.Api.prototype.ajax,
+ ajaxCalled = false,
+ fakeJqxhr = {};
+
+ mw.Api.prototype.ajax = function () {
+ ajaxCalled = true;
+ return $.Deferred().resolve( {}, fakeJqxhr );
+ };
+
+ mw.mmv.logging.PerformanceLogger.prototype.recordJQueryEntryDelayed = function ( type, total, jqxhr ) {
+ assert.strictEqual( type, 'foo', 'type was passed correctly' );
+ assert.strictEqual( jqxhr, fakeJqxhr, 'jqXHR was passed correctly' );
+ };
+
+ api = new mw.mmv.logging.Api( 'foo' );
+
+ api.ajax();
+
+ assert.ok( ajaxCalled, 'parent ajax() function was called' );
+
+ mw.mmv.logging.PerformanceLogger.prototype.recordJQueryEntryDelayed = oldRecord;
+ mw.Api.prototype.ajax = oldAjax;
+ } );
+}( mediaWiki, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/logging/mmv.logging.ViewLogger.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/logging/mmv.logging.ViewLogger.test.js
new file mode 100644
index 00000000..5da3633f
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/logging/mmv.logging.ViewLogger.test.js
@@ -0,0 +1,87 @@
+( function ( mw, $ ) {
+ QUnit.module( 'mmv.logging.ViewLogger', QUnit.newMwEnvironment( {
+ setup: function () {
+ this.clock = this.sandbox.useFakeTimers();
+
+ // since jQuery 2/3, $.now will capture a reference to Date.now
+ // before above fake timer gets a chance to override it, so I'll
+ // override that new behavior in order to run these tests...
+ // @see https://github.com/sinonjs/lolex/issues/76
+ this.oldNow = $.now;
+ $.now = function () { return +( new Date() ); };
+ },
+
+ teardown: function () {
+ $.now = this.oldNow;
+ this.clock.restore();
+ }
+ } ) );
+
+ QUnit.test( 'unview()', function ( assert ) {
+ var logger = { log: $.noop },
+ viewLogger = new mw.mmv.logging.ViewLogger( { recordVirtualViewBeaconURI: $.noop }, {}, logger );
+
+ this.sandbox.stub( logger, 'log' );
+
+ viewLogger.unview();
+
+ assert.ok( !logger.log.called, 'action logger not called' );
+
+ viewLogger.setLastViewLogged( false );
+ viewLogger.unview();
+
+ assert.ok( !logger.log.called, 'action logger not called' );
+
+ viewLogger.setLastViewLogged( true );
+ viewLogger.unview();
+
+ assert.ok( logger.log.calledOnce, 'action logger called' );
+
+ viewLogger.unview();
+
+ assert.ok( logger.log.calledOnce, 'action logger not called again' );
+ } );
+
+ QUnit.test( 'focus and blur', function ( assert ) {
+ var fakeWindow = $( '<div>' ),
+ viewLogger = new mw.mmv.logging.ViewLogger( { recordVirtualViewBeaconURI: $.noop }, fakeWindow, { log: $.noop } );
+
+ this.clock.tick( 1 ); // This is just so that $.now() > 0 in the fake timer environment
+
+ viewLogger.attach();
+
+ this.clock.tick( 5 );
+
+ fakeWindow.triggerHandler( 'blur' );
+
+ this.clock.tick( 2 );
+
+ fakeWindow.triggerHandler( 'focus' );
+
+ this.clock.tick( 3 );
+
+ fakeWindow.triggerHandler( 'blur' );
+
+ this.clock.tick( 4 );
+
+ assert.strictEqual( viewLogger.viewDuration, 8, 'Only focus duration was logged' );
+ } );
+
+ QUnit.test( 'stopViewDuration before startViewDuration', function ( assert ) {
+ var viewLogger = new mw.mmv.logging.ViewLogger( { recordVirtualViewBeaconURI: $.noop }, {}, { log: $.noop } );
+
+ this.clock.tick( 1 ); // This is just so that $.now() > 0 in the fake timer environment
+
+ viewLogger.stopViewDuration();
+
+ this.clock.tick( 2 );
+
+ viewLogger.startViewDuration();
+
+ this.clock.tick( 3 );
+
+ viewLogger.stopViewDuration();
+
+ assert.strictEqual( viewLogger.viewDuration, 3, 'Only last timeframe was logged' );
+ } );
+}( mediaWiki, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.Config.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.Config.test.js
new file mode 100644
index 00000000..0a1b6a61
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.Config.test.js
@@ -0,0 +1,203 @@
+/*
+ * This file is part of the MediaWiki extension MediaViewer.
+ *
+ * MediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $ ) {
+ QUnit.module( 'mmv.Config', QUnit.newMwEnvironment() );
+
+ QUnit.test( 'Constructor sanity test', function ( assert ) {
+ var config = new mw.mmv.Config( {}, {}, {}, {}, null );
+ assert.ok( config );
+ } );
+
+ QUnit.test( 'Localstorage get', function ( assert ) {
+ var localStorage, config;
+
+ localStorage = mw.mmv.testHelpers.getUnsupportedLocalStorage(); // no browser support
+ config = new mw.mmv.Config( {}, {}, {}, {}, localStorage );
+ assert.strictEqual( config.getFromLocalStorage( 'foo' ), null, 'Returns null when not supported' );
+ assert.strictEqual( config.getFromLocalStorage( 'foo', 'bar' ), 'bar', 'Returns fallback when not supported' );
+
+ localStorage = mw.mmv.testHelpers.getDisabledLocalStorage(); // browser supports it but disabled
+ config = new mw.mmv.Config( {}, {}, {}, {}, localStorage );
+ assert.strictEqual( config.getFromLocalStorage( 'foo' ), null, 'Returns null when disabled' );
+ assert.strictEqual( config.getFromLocalStorage( 'foo', 'bar' ), 'bar', 'Returns fallback when disabled' );
+
+ localStorage = mw.mmv.testHelpers.createLocalStorage( { getItem: this.sandbox.stub() } );
+ config = new mw.mmv.Config( {}, {}, {}, {}, localStorage );
+
+ localStorage.store.getItem.withArgs( 'foo' ).returns( null );
+ assert.strictEqual( config.getFromLocalStorage( 'foo' ), null, 'Returns null when key not set' );
+ assert.strictEqual( config.getFromLocalStorage( 'foo', 'bar' ), 'bar', 'Returns fallback when key not set' );
+
+ localStorage.store.getItem.reset();
+ localStorage.store.getItem.withArgs( 'foo' ).returns( 'boom' );
+ assert.strictEqual( config.getFromLocalStorage( 'foo' ), 'boom', 'Returns correct value' );
+ assert.strictEqual( config.getFromLocalStorage( 'foo', 'bar' ), 'boom', 'Returns correct value ignoring fallback' );
+ } );
+
+ QUnit.test( 'Localstorage set', function ( assert ) {
+ var localStorage, config;
+
+ localStorage = mw.mmv.testHelpers.getUnsupportedLocalStorage(); // no browser support
+ config = new mw.mmv.Config( {}, {}, {}, {}, localStorage );
+ assert.strictEqual( config.setInLocalStorage( 'foo', 'bar' ), false, 'Returns false when not supported' );
+
+ localStorage = mw.mmv.testHelpers.getDisabledLocalStorage(); // browser supports it but disabled
+ config = new mw.mmv.Config( {}, {}, {}, {}, localStorage );
+ assert.strictEqual( config.setInLocalStorage( 'foo', 'bar' ), false, 'Returns false when disabled' );
+
+ localStorage = mw.mmv.testHelpers.createLocalStorage( { setItem: this.sandbox.stub() } );
+ config = new mw.mmv.Config( {}, {}, {}, {}, localStorage );
+
+ assert.strictEqual( config.setInLocalStorage( 'foo', 'bar' ), true, 'Returns true when works' );
+
+ localStorage.store.setItem.throwsException( 'localStorage full!' );
+ assert.strictEqual( config.setInLocalStorage( 'foo', 'bar' ), false, 'Returns false on error' );
+ } );
+
+ QUnit.test( 'Localstorage remove', function ( assert ) {
+ var localStorage, config;
+
+ localStorage = mw.mmv.testHelpers.getUnsupportedLocalStorage(); // no browser support
+ config = new mw.mmv.Config( {}, {}, {}, {}, localStorage );
+ assert.strictEqual( config.removeFromLocalStorage( 'foo' ), true, 'Returns true when not supported' );
+
+ localStorage = mw.mmv.testHelpers.getDisabledLocalStorage(); // browser supports it but disabled
+ config = new mw.mmv.Config( {}, {}, {}, {}, localStorage );
+ assert.strictEqual( config.removeFromLocalStorage( 'foo' ), true, 'Returns true when disabled' );
+
+ localStorage = mw.mmv.testHelpers.createLocalStorage( { removeItem: this.sandbox.stub() } );
+ config = new mw.mmv.Config( {}, {}, {}, {}, localStorage );
+ assert.strictEqual( config.removeFromLocalStorage( 'foo' ), true, 'Returns true when works' );
+ } );
+
+ QUnit.test( 'isMediaViewerEnabledOnClick', function ( assert ) {
+ var localStorage = mw.mmv.testHelpers.createLocalStorage( { getItem: this.sandbox.stub() } ),
+ mwConfig = { get: this.sandbox.stub() },
+ mwUser = { isAnon: this.sandbox.stub() },
+ config = new mw.mmv.Config( {}, mwConfig, mwUser, {}, localStorage );
+
+ mwUser.isAnon.returns( false );
+ mwConfig.get.withArgs( 'wgMediaViewer' ).returns( true );
+ mwConfig.get.withArgs( 'wgMediaViewerOnClick' ).returns( true );
+ assert.strictEqual( config.isMediaViewerEnabledOnClick(), true, 'Returns true for logged-in with standard settings' );
+
+ mwUser.isAnon.returns( false );
+ mwConfig.get.withArgs( 'wgMediaViewer' ).returns( false );
+ mwConfig.get.withArgs( 'wgMediaViewerOnClick' ).returns( true );
+ assert.strictEqual( config.isMediaViewerEnabledOnClick(), false, 'Returns false if opted out via user JS flag' );
+
+ mwUser.isAnon.returns( false );
+ mwConfig.get.withArgs( 'wgMediaViewer' ).returns( true );
+ mwConfig.get.withArgs( 'wgMediaViewerOnClick' ).returns( false );
+ assert.strictEqual( config.isMediaViewerEnabledOnClick(), false, 'Returns false if opted out via preferences' );
+
+ mwUser.isAnon.returns( true );
+ mwConfig.get.withArgs( 'wgMediaViewer' ).returns( false );
+ mwConfig.get.withArgs( 'wgMediaViewerOnClick' ).returns( true );
+ assert.strictEqual( config.isMediaViewerEnabledOnClick(), false, 'Returns false if anon user opted out via user JS flag' );
+
+ mwUser.isAnon.returns( true );
+ mwConfig.get.withArgs( 'wgMediaViewer' ).returns( true );
+ mwConfig.get.withArgs( 'wgMediaViewerOnClick' ).returns( false );
+ assert.strictEqual( config.isMediaViewerEnabledOnClick(), false, 'Returns false if anon user opted out in some weird way' ); // apparently someone created a browser extension to do this
+
+ mwUser.isAnon.returns( true );
+ mwConfig.get.withArgs( 'wgMediaViewer' ).returns( true );
+ mwConfig.get.withArgs( 'wgMediaViewerOnClick' ).returns( true );
+ localStorage.store.getItem.withArgs( 'wgMediaViewerOnClick' ).returns( null );
+ assert.strictEqual( config.isMediaViewerEnabledOnClick(), true, 'Returns true for anon with standard settings' );
+
+ mwUser.isAnon.returns( true );
+ mwConfig.get.withArgs( 'wgMediaViewer' ).returns( true );
+ mwConfig.get.withArgs( 'wgMediaViewerOnClick' ).returns( true );
+ localStorage.store.getItem.withArgs( 'wgMediaViewerOnClick' ).returns( '0' );
+ assert.strictEqual( config.isMediaViewerEnabledOnClick(), false, 'Returns true for anon opted out via localSettings' );
+ } );
+
+ QUnit.test( 'setMediaViewerEnabledOnClick sanity check', function ( assert ) {
+ var localStorage = mw.mmv.testHelpers.createLocalStorage( {
+ getItem: this.sandbox.stub(),
+ setItem: this.sandbox.stub(),
+ removeItem: this.sandbox.stub()
+ } ),
+ mwUser = { isAnon: this.sandbox.stub() },
+ mwConfig = new mw.Map(),
+ api = { saveOption: this.sandbox.stub().returns( $.Deferred().resolve() ) },
+ config = new mw.mmv.Config( {}, mwConfig, mwUser, api, localStorage );
+ mwConfig.set( 'wgMediaViewerEnabledByDefault', false );
+
+ mwUser.isAnon.returns( false );
+ api.saveOption.returns( $.Deferred().resolve() );
+ config.setMediaViewerEnabledOnClick( false );
+ assert.ok( api.saveOption.called, 'For logged-in users, pref change is via API' );
+
+ mwUser.isAnon.returns( true );
+ config.setMediaViewerEnabledOnClick( false );
+ assert.ok( localStorage.store.setItem.called, 'For anons, opt-out is set in localStorage' );
+
+ mwUser.isAnon.returns( true );
+ config.setMediaViewerEnabledOnClick( true );
+ assert.ok( localStorage.store.removeItem.called, 'For anons, opt-in means clearing localStorage' );
+ } );
+
+ QUnit.test( 'shouldShowStatusInfo', function ( assert ) {
+ var config,
+ mwConfig = new mw.Map(),
+ fakeLocalStorage = mw.mmv.testHelpers.getFakeLocalStorage(),
+ mwUser = { isAnon: this.sandbox.stub() },
+ api = { saveOption: this.sandbox.stub().returns( $.Deferred().resolve() ) };
+
+ mwConfig.set( {
+ wgMediaViewer: true,
+ wgMediaViewerOnClick: true,
+ wgMediaViewerEnabledByDefault: true
+ } );
+ config = new mw.mmv.Config( {}, mwConfig, mwUser, api, fakeLocalStorage );
+ mwUser.isAnon.returns( false );
+
+ assert.strictEqual( config.shouldShowStatusInfo(), false, 'Status info is not shown by default' );
+ config.setMediaViewerEnabledOnClick( false );
+ assert.strictEqual( config.shouldShowStatusInfo(), true, 'Status info is shown after MMV is disabled the first time' );
+ config.setMediaViewerEnabledOnClick( true );
+ assert.strictEqual( config.shouldShowStatusInfo(), false, 'Status info is not shown when MMV is enabled' );
+ config.setMediaViewerEnabledOnClick( false );
+ assert.strictEqual( config.shouldShowStatusInfo(), true, 'Status info is shown after MMV is disabled the first time #2' );
+ config.disableStatusInfo();
+ assert.strictEqual( config.shouldShowStatusInfo(), false, 'Status info is not shown when already displayed once' );
+ config.setMediaViewerEnabledOnClick( true );
+ assert.strictEqual( config.shouldShowStatusInfo(), false, 'Further status changes have no effect' );
+ config.setMediaViewerEnabledOnClick( false );
+ assert.strictEqual( config.shouldShowStatusInfo(), false, 'Further status changes have no effect #2' );
+
+ // make sure disabling calls maybeEnableStatusInfo() for logged-in as well
+ config.localStorage = mw.mmv.testHelpers.getFakeLocalStorage();
+ mwUser.isAnon.returns( true );
+ assert.strictEqual( config.shouldShowStatusInfo(), false, 'Status info is not shown by default for logged-in users' );
+ config.setMediaViewerEnabledOnClick( false );
+ assert.strictEqual( config.shouldShowStatusInfo(), true, 'Status info is shown after MMV is disabled the first time for logged-in users' );
+
+ // make sure popup is not shown immediately on disabled-by-default sites, but still works otherwise
+ config.localStorage = mw.mmv.testHelpers.getFakeLocalStorage();
+ mwConfig.set( 'wgMediaViewerEnabledByDefault', false );
+ assert.strictEqual( config.shouldShowStatusInfo(), false, 'Status info is not shown by default #2' );
+ config.setMediaViewerEnabledOnClick( true );
+ assert.strictEqual( config.shouldShowStatusInfo(), false, 'Status info is not shown when MMV is enabled #2' );
+ config.setMediaViewerEnabledOnClick( false );
+ assert.strictEqual( config.shouldShowStatusInfo(), true, 'Status info is shown after MMV is disabled the first time #2' );
+ } );
+}( mediaWiki, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.EmbedFileFormatter.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.EmbedFileFormatter.test.js
new file mode 100644
index 00000000..d215eaf5
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.EmbedFileFormatter.test.js
@@ -0,0 +1,293 @@
+( function ( mw ) {
+ QUnit.module( 'mmv.EmbedFileFormatter', QUnit.newMwEnvironment() );
+
+ function createEmbedFileInfo( options ) {
+ var license = options.licenseShortName ? new mw.mmv.model.License( options.licenseShortName,
+ options.licenseInternalName, options.licenseLongName, options.licenseUrl ) : undefined,
+ imageInfo = new mw.mmv.model.Image(
+
+ options.title,
+ options.title.getNameText(),
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ options.imgUrl,
+ options.filePageUrl,
+ options.shortFilePageUrl,
+ 42,
+ 'repo',
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ options.source,
+ options.author,
+ options.authorCount,
+ license ),
+ repoInfo = { displayName: options.siteName, getSiteLink:
+ function () { return options.siteUrl; } };
+
+ return new mw.mmv.model.EmbedFileInfo( imageInfo, repoInfo, options.caption );
+ }
+
+ QUnit.test( 'EmbedFileFormatter constructor sanity check', function ( assert ) {
+ var formatter = new mw.mmv.EmbedFileFormatter();
+ assert.ok( formatter, 'constructor with no argument works' );
+ } );
+
+ QUnit.test( 'getByline():', function ( assert ) {
+ var formatter = new mw.mmv.EmbedFileFormatter(),
+ author = '<span class="mw-mmv-author">Homer</span>',
+ source = '<span class="mw-mmv-source">Iliad</span>',
+ attribution = '<span class="mw-mmv-attr">Cat</span>',
+ byline;
+
+ // Works with no arguments
+ byline = formatter.getByline();
+ assert.strictEqual( byline, undefined, 'No argument case handled correctly.' );
+
+ // Attribution present
+ byline = formatter.getByline( author, source, attribution );
+ assert.ok( byline.match( /Cat/ ), 'Attribution found in bylines' );
+
+ // Author and source present
+ byline = formatter.getByline( author, source );
+ assert.ok( byline.match( /Homer|Iliad/ ), 'Author and source found in bylines' );
+
+ // Only author present
+ byline = formatter.getByline( author );
+ assert.ok( byline.match( /Homer/ ), 'Author found in bylines.' );
+
+ // Only source present
+ byline = formatter.getByline( undefined, source );
+ assert.ok( byline.match( /Iliad/ ), 'Source found in bylines.' );
+ } );
+
+ QUnit.test( 'getSiteLink():', function ( assert ) {
+ var repoInfo = new mw.mmv.model.Repo( 'Wikipedia', '//wikipedia.org/favicon.ico', true ),
+ info = new mw.mmv.model.EmbedFileInfo( {}, repoInfo ),
+ formatter = new mw.mmv.EmbedFileFormatter(),
+ siteUrl = repoInfo.getSiteLink(),
+ siteLink = formatter.getSiteLink( info );
+
+ assert.ok( siteLink.match( 'Wikipedia' ), 'Site name is present in site link' );
+ assert.ok( siteLink.indexOf( siteUrl ) !== -1, 'Site URL is present in site link' );
+ } );
+
+ QUnit.test( 'getThumbnailHtml():', function ( assert ) {
+ var formatter = new mw.mmv.EmbedFileFormatter(),
+ titleText = 'Music Room',
+ title = mw.Title.newFromText( titleText ),
+ imgUrl = 'https://upload.wikimedia.org/wikipedia/commons/3/3a/Foobar.jpg',
+ filePageUrl = 'https://commons.wikimedia.org/wiki/File:Foobar.jpg',
+ filePageShortUrl = 'https://commons.wikimedia.org/wiki/index.php?curid=42',
+ siteName = 'Site Name',
+ siteUrl = '//site.url/',
+ licenseShortName = 'Public License',
+ licenseInternalName = '-',
+ licenseLongName = 'Public Domain, copyrights have lapsed',
+ licenseUrl = '//example.com/pd',
+ author = '<span class="mw-mmv-author">Homer</span>',
+ source = '<span class="mw-mmv-source">Iliad</span>',
+ thumbUrl = 'https://upload.wikimedia.org/wikipedia/thumb/Foobar.jpg',
+ width = 700,
+ height = 500,
+ info,
+ generatedHtml;
+
+ // Bylines, license and site
+ info = createEmbedFileInfo( { title: title, imgUrl: imgUrl, filePageUrl: filePageUrl,
+ shortFilePageUrl: filePageShortUrl, siteName: siteName, siteUrl: siteUrl,
+ licenseShortName: licenseShortName, licenseInternalName: licenseInternalName,
+ licenseLongName: licenseLongName, licenseUrl: licenseUrl, author: author, source: source } );
+
+ generatedHtml = formatter.getThumbnailHtml( info, thumbUrl, width, height );
+ assert.ok( generatedHtml.match( titleText ), 'Title appears in generated HTML.' );
+ assert.ok( generatedHtml.match( filePageUrl ), 'Page url appears in generated HTML.' );
+ assert.ok( generatedHtml.match( thumbUrl ), 'Thumbnail url appears in generated HTML' );
+ assert.ok( generatedHtml.match( 'Public License' ), 'License appears in generated HTML' );
+ assert.ok( generatedHtml.match( 'Homer' ), 'Author appears in generated HTML' );
+ assert.ok( generatedHtml.match( 'Iliad' ), 'Source appears in generated HTML' );
+ assert.ok( generatedHtml.match( width ), 'Width appears in generated HTML' );
+ assert.ok( generatedHtml.match( height ), 'Height appears in generated HTML' );
+ // .includes() for checking the short url since it contains a ? (bad for regex). Could escape instead.
+ assert.ok( generatedHtml.includes( filePageShortUrl ), 'Short URL appears in generated HTML' );
+
+ // Bylines, no license and site
+ info = createEmbedFileInfo( { title: title, imgUrl: imgUrl, filePageUrl: filePageUrl,
+ shortFilePageUrl: filePageShortUrl, siteName: siteName, siteUrl: siteUrl,
+ author: author, source: source } );
+ generatedHtml = formatter.getThumbnailHtml( info, thumbUrl, width, height );
+
+ assert.ok( generatedHtml.match( titleText ), 'Title appears in generated HTML.' );
+ assert.ok( generatedHtml.match( filePageUrl ), 'Page url appears in generated HTML.' );
+ assert.ok( generatedHtml.match( thumbUrl ), 'Thumbnail url appears in generated HTML' );
+ assert.ok( !generatedHtml.match( 'Public License' ), 'License should not appear in generated HTML' );
+ assert.ok( generatedHtml.match( 'Homer' ), 'Author appears in generated HTML' );
+ assert.ok( generatedHtml.match( 'Iliad' ), 'Source appears in generated HTML' );
+ assert.ok( generatedHtml.match( width ), 'Width appears in generated HTML' );
+ assert.ok( generatedHtml.match( height ), 'Height appears in generated HTML' );
+ assert.ok( generatedHtml.includes( filePageShortUrl ), 'Short URL appears in generated HTML' );
+
+ // No bylines, license and site
+ info = createEmbedFileInfo( { title: title, imgUrl: imgUrl, filePageUrl: filePageUrl,
+ siteName: siteName, siteUrl: siteUrl, licenseShortName: licenseShortName,
+ licenseInternalName: licenseInternalName, licenseLongName: licenseLongName,
+ licenseUrl: licenseUrl, shortFilePageUrl: filePageShortUrl } );
+ generatedHtml = formatter.getThumbnailHtml( info, thumbUrl, width, height );
+
+ assert.ok( generatedHtml.match( titleText ), 'Title appears in generated HTML.' );
+ assert.ok( generatedHtml.match( filePageUrl ), 'Page url appears in generated HTML.' );
+ assert.ok( generatedHtml.match( thumbUrl ), 'Thumbnail url appears in generated HTML' );
+ assert.ok( generatedHtml.match( 'Public License' ), 'License appears in generated HTML' );
+ assert.ok( !generatedHtml.match( 'Homer' ), 'Author should not appear in generated HTML' );
+ assert.ok( !generatedHtml.match( 'Iliad' ), 'Source should not appear in generated HTML' );
+ assert.ok( generatedHtml.match( width ), 'Width appears in generated HTML' );
+ assert.ok( generatedHtml.match( height ), 'Height appears in generated HTML' );
+ assert.ok( generatedHtml.includes( filePageShortUrl ), 'Short URL appears in generated HTML' );
+
+ // No bylines, no license and site
+ info = createEmbedFileInfo( { title: title, imgUrl: imgUrl, filePageUrl: filePageUrl,
+ siteName: siteName, siteUrl: siteUrl, shortFilePageUrl: filePageShortUrl } );
+ generatedHtml = formatter.getThumbnailHtml( info, thumbUrl, width, height );
+
+ assert.ok( generatedHtml.match( titleText ), 'Title appears in generated HTML.' );
+ assert.ok( generatedHtml.match( filePageUrl ), 'Page url appears in generated HTML.' );
+ assert.ok( generatedHtml.match( thumbUrl ), 'Thumbnail url appears in generated HTML' );
+ assert.ok( !generatedHtml.match( 'Public License' ), 'License should not appear in generated HTML' );
+ assert.ok( !generatedHtml.match( 'Homer' ), 'Author should not appear in generated HTML' );
+ assert.ok( !generatedHtml.match( 'Iliad' ), 'Source should not appear in generated HTML' );
+ assert.ok( generatedHtml.match( width ), 'Width appears in generated HTML' );
+ assert.ok( generatedHtml.match( height ), 'Height appears in generated HTML' );
+ assert.ok( generatedHtml.includes( filePageShortUrl ), 'Short URL appears in generated HTML' );
+
+ } );
+
+ QUnit.test( 'getThumbnailWikitext():', function ( assert ) {
+ var formatter = new mw.mmv.EmbedFileFormatter(),
+ title = mw.Title.newFromText( 'File:Foobar.jpg' ),
+ imgUrl = 'https://upload.wikimedia.org/wikipedia/commons/3/3a/Foobar.jpg',
+ filePageUrl = 'https://commons.wikimedia.org/wiki/File:Foobar.jpg',
+ caption = 'Foobar caption.',
+ width = 700,
+ info,
+ wikitext;
+
+ // Title, width and caption
+ info = createEmbedFileInfo( { title: title, imgUrl: imgUrl, filePageUrl: filePageUrl,
+ caption: caption } );
+ wikitext = formatter.getThumbnailWikitextFromEmbedFileInfo( info, width );
+
+ assert.strictEqual(
+ wikitext,
+ '[[File:Foobar.jpg|700px|thumb|Foobar caption.]]',
+ 'Wikitext generated correctly.' );
+
+ // Title, width and no caption
+ info = createEmbedFileInfo( { title: title, imgUrl: imgUrl, filePageUrl: filePageUrl } );
+ wikitext = formatter.getThumbnailWikitextFromEmbedFileInfo( info, width );
+
+ assert.strictEqual(
+ wikitext,
+ '[[File:Foobar.jpg|700px|thumb|Foobar]]',
+ 'Wikitext generated correctly.' );
+
+ // Title, no width and no caption
+ info = createEmbedFileInfo( { title: title, imgUrl: imgUrl, filePageUrl: filePageUrl } );
+ wikitext = formatter.getThumbnailWikitextFromEmbedFileInfo( info );
+
+ assert.strictEqual(
+ wikitext,
+ '[[File:Foobar.jpg|thumb|Foobar]]',
+ 'Wikitext generated correctly.' );
+ } );
+
+ QUnit.test( 'getCreditText():', function ( assert ) {
+ var txt, formatter = new mw.mmv.EmbedFileFormatter();
+
+ txt = formatter.getCreditText( {
+ repoInfo: {
+ displayName: 'Localcommons'
+ },
+
+ imageInfo: {
+ author: 'Author',
+ source: 'Source',
+ descriptionShortUrl: 'link',
+ title: {
+ getNameText: function () { return 'Image Title'; }
+ }
+ }
+ } );
+
+ assert.strictEqual( txt, 'By Author - Source, link', 'Sanity check' );
+
+ txt = formatter.getCreditText( {
+ repoInfo: {
+ displayName: 'Localcommons'
+ },
+
+ imageInfo: {
+ author: 'Author',
+ source: 'Source',
+ descriptionShortUrl: 'link',
+ title: {
+ getNameText: function () { return 'Image Title'; }
+ },
+ license: {
+ getShortName: function () { return 'WTFPL v2'; },
+ longName: 'Do What the Fuck You Want Public License Version 2',
+ isFree: this.sandbox.stub().returns( true )
+ }
+ }
+ } );
+
+ assert.strictEqual( txt, 'By Author - Source, WTFPL v2, link', 'License message works' );
+ } );
+
+ QUnit.test( 'getCreditHtml():', function ( assert ) {
+ var html, formatter = new mw.mmv.EmbedFileFormatter();
+
+ html = formatter.getCreditHtml( {
+ repoInfo: {
+ displayName: 'Localcommons',
+ getSiteLink: function () { return 'quux'; }
+ },
+
+ imageInfo: {
+ author: 'Author',
+ source: 'Source',
+ descriptionShortUrl: 'some link',
+ title: {
+ getNameText: function () { return 'Image Title'; }
+ }
+ }
+ } );
+
+ assert.strictEqual( html, 'By Author - Source, <a href="some link">Link</a>', 'Sanity check' );
+
+ html = formatter.getCreditHtml( {
+ repoInfo: {
+ displayName: 'Localcommons',
+ getSiteLink: function () { return 'quux'; }
+ },
+
+ imageInfo: {
+ author: 'Author',
+ source: 'Source',
+ descriptionShortUrl: 'some link',
+ title: {
+ getNameText: function () { return 'Image Title'; }
+ },
+ license: {
+ getShortLink: function () { return '<a href="http://www.wtfpl.net/">WTFPL v2</a>'; },
+ longName: 'Do What the Fuck You Want Public License Version 2',
+ isFree: this.sandbox.stub().returns( true )
+ }
+ }
+ } );
+
+ assert.strictEqual( html, 'By Author - Source, <a href="http://www.wtfpl.net/">WTFPL v2</a>, <a href="some link">Link</a>', 'Sanity check' );
+ } );
+}( mediaWiki ) );
diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.HtmlUtils.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.HtmlUtils.test.js
new file mode 100644
index 00000000..48c0076f
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.HtmlUtils.test.js
@@ -0,0 +1,192 @@
+/*
+ * This file is part of the MediaWiki extension MediaViewer.
+ *
+ * MediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $ ) {
+ QUnit.module( 'mmv.HtmlUtils', QUnit.newMwEnvironment() );
+
+ QUnit.test( 'wrapAndJquerify() for single node', function ( assert ) {
+ var utils = new mw.mmv.HtmlUtils(),
+ $el = $( '<span>' ),
+ el = $( '<span>' ).get( 0 ),
+ html = '<span></span>',
+ invalid = {};
+
+ assert.strictEqual( utils.wrapAndJquerify( $el ).html(), '<span></span>', 'jQuery' );
+ assert.strictEqual( utils.wrapAndJquerify( el ).html(), '<span></span>', 'HTMLElement' );
+ assert.strictEqual( utils.wrapAndJquerify( html ).html(), '<span></span>', 'HTML string' );
+
+ try {
+ utils.wrapAndJquerify( invalid );
+ } catch ( e ) {
+ assert.ok( e, 'throws exception for invalid type' );
+ }
+ } );
+
+ QUnit.test( 'wrapAndJquerify() for multiple nodes', function ( assert ) {
+ var utils = new mw.mmv.HtmlUtils(),
+ $el = $( '<span></span><span></span>' ),
+ html = '<span></span><span></span>';
+
+ assert.strictEqual( utils.wrapAndJquerify( $el ).html(), '<span></span><span></span>', 'jQuery' );
+ assert.strictEqual( utils.wrapAndJquerify( html ).html(), '<span></span><span></span>', 'HTML string' );
+ } );
+
+ QUnit.test( 'wrapAndJquerify() for text', function ( assert ) {
+ var utils = new mw.mmv.HtmlUtils(),
+ $el = $( document.createTextNode( 'foo' ) ),
+ html = 'foo';
+
+ assert.strictEqual( utils.wrapAndJquerify( $el ).html(), 'foo', 'jQuery' );
+ assert.strictEqual( utils.wrapAndJquerify( html ).html(), 'foo', 'HTML string' );
+ } );
+
+ QUnit.test( 'wrapAndJquerify() does not change original', function ( assert ) {
+ var utils = new mw.mmv.HtmlUtils(),
+ $el = $( '<span>' ),
+ el = $( '<span>' ).get( 0 );
+
+ utils.wrapAndJquerify( $el ).find( 'span' ).prop( 'data-x', 1 );
+ utils.wrapAndJquerify( el ).find( 'span' ).prop( 'data-x', 1 );
+ assert.strictEqual( $el.prop( 'data-x' ), undefined, 'wrapped jQuery element is not the same as original' );
+ assert.strictEqual( $( el ).prop( 'data-x' ), undefined, 'wrapped HTMLElement is not the same as original' );
+ } );
+
+ QUnit.test( 'filterInvisible()', function ( assert ) {
+ var utils = new mw.mmv.HtmlUtils(),
+ $visibleChild = $( '<div><span></span></div>' ),
+ $invisibleChild = $( '<div><span style="display: none"></span></div>' ),
+ $invisibleChildInVisibleChild = $( '<div><span><abbr style="display: none"></abbr></span></div>' ),
+ $visibleChildInInvisibleChild = $( '<div><span style="display: none"><abbr></abbr></span></div>' ),
+ $invisibleChildWithVisibleSiblings = $( '<div><span></span><abbr style="display: none"></abbr><b></b></div>' );
+
+ utils.filterInvisible( $visibleChild );
+ utils.filterInvisible( $invisibleChild );
+ utils.filterInvisible( $invisibleChildInVisibleChild );
+ utils.filterInvisible( $visibleChildInInvisibleChild );
+ utils.filterInvisible( $invisibleChildWithVisibleSiblings );
+
+ assert.ok( $visibleChild.has( 'span' ).length, 'visible child is not filtered' );
+ assert.ok( !$invisibleChild.has( 'span' ).length, 'invisible child is filtered' );
+ assert.ok( $invisibleChildInVisibleChild.has( 'span' ).length, 'visible child is not filtered...' );
+ assert.ok( !$invisibleChildInVisibleChild.has( 'abbr' ).length, '... but its invisible child is' );
+ assert.ok( !$visibleChildInInvisibleChild.has( 'span' ).length, 'invisible child is filtered...' );
+ assert.ok( !$visibleChildInInvisibleChild.has( 'abbr' ).length, '...and its children too' );
+ assert.ok( $visibleChild.has( 'span' ).length, 'visible child is not filtered' );
+ assert.ok( !$invisibleChildWithVisibleSiblings.has( 'abbr' ).length, 'invisible sibling is filtered...' );
+ assert.ok( $invisibleChildWithVisibleSiblings.has( 'span' ).length, '...but its visible siblings are not' );
+ assert.ok( $invisibleChildWithVisibleSiblings.has( 'b' ).length, '...but its visible siblings are not' );
+ } );
+
+ QUnit.test( 'whitelistHtml()', function ( assert ) {
+ var utils = new mw.mmv.HtmlUtils(),
+ $whitelisted = $( '<div>abc<a>def</a>ghi</div>' ),
+ $nonWhitelisted = $( '<div>abc<span>def</span>ghi</div>' ),
+ $nonWhitelistedInWhitelisted = $( '<div>abc<a>d<span>e</span>f</a>ghi</div>' ),
+ $whitelistedInNonWhitelisted = $( '<div>abc<span>d<a>e</a>f</span>ghi</div>' ),
+ $siblings = $( '<div>ab<span>c</span>d<a>e</a>f<span>g</span>hi</div>' );
+
+ utils.whitelistHtml( $whitelisted, 'a' );
+ utils.whitelistHtml( $nonWhitelisted, 'a' );
+ utils.whitelistHtml( $nonWhitelistedInWhitelisted, 'a' );
+ utils.whitelistHtml( $whitelistedInNonWhitelisted, 'a' );
+ utils.whitelistHtml( $siblings, 'a' );
+
+ assert.ok( $whitelisted.has( 'a' ).length, 'Whitelisted elements are kept.' );
+ assert.ok( !$nonWhitelisted.has( 'span' ).length, 'Non-whitelisted elements are removed.' );
+ assert.ok( $nonWhitelistedInWhitelisted.has( 'a' ).length, 'Whitelisted parents are kept.' );
+ assert.ok( !$nonWhitelistedInWhitelisted.has( 'span' ).length, 'Non-whitelisted children are removed.' );
+ assert.ok( !$whitelistedInNonWhitelisted.has( 'span' ).length, 'Non-whitelisted parents are removed.' );
+ assert.ok( $whitelistedInNonWhitelisted.has( 'a' ).length, 'Whitelisted children are kept.' );
+ assert.ok( !$siblings.has( 'span' ).length, 'Non-whitelisted siblings are removed.' );
+ assert.ok( $siblings.has( 'a' ).length, 'Whitelisted siblings are kept.' );
+ } );
+
+ QUnit.test( 'appendWhitespaceToBlockElements()', function ( assert ) {
+ var utils = new mw.mmv.HtmlUtils(),
+ $noBlockElement = $( '<div>abc<i>def</i>ghi</div>' ),
+ $blockElement = $( '<div>abc<p>def</p>ghi</div>' ),
+ $linebreak = $( '<div>abc<br>def</div>' );
+
+ utils.appendWhitespaceToBlockElements( $noBlockElement );
+ utils.appendWhitespaceToBlockElements( $blockElement );
+ utils.appendWhitespaceToBlockElements( $linebreak );
+
+ assert.ok( $noBlockElement.text().match( /abcdefghi/ ), 'Non-block elemens are not whitespaced.' );
+ assert.ok( $blockElement.text().match( /abc\s+def\s+ghi/ ), 'Block elemens are whitespaced.' );
+ assert.ok( $linebreak.text().match( /abc\s+def/ ), 'Linebreaks are whitespaced.' );
+ } );
+
+ QUnit.test( 'jqueryToHtml()', function ( assert ) {
+ var utils = new mw.mmv.HtmlUtils();
+
+ assert.strictEqual( utils.jqueryToHtml( $( '<a>' ) ), '<a></a>',
+ 'works for single element' );
+ assert.strictEqual( utils.jqueryToHtml( $( '<b><a>foo</a></b>' ) ), '<b><a>foo</a></b>',
+ 'works for complex element' );
+ assert.strictEqual( utils.jqueryToHtml( $( '<a>foo</a>' ).contents() ), 'foo',
+ 'works for text nodes' );
+ } );
+
+ QUnit.test( 'mergeWhitespace()', function ( assert ) {
+ var utils = new mw.mmv.HtmlUtils();
+
+ assert.strictEqual( utils.mergeWhitespace( ' x \n' ), 'x',
+ 'leading/trainling whitespace is trimmed' );
+ assert.strictEqual( utils.mergeWhitespace( 'x \n\n \n y' ), 'x\ny',
+ 'whitespace containing a newline is collapsed into a single newline' );
+ assert.strictEqual( utils.mergeWhitespace( 'x y' ), 'x y',
+ 'multiple spaces are collapsed into a single one' );
+ } );
+
+ QUnit.test( 'htmlToText()', function ( assert ) {
+ var utils = new mw.mmv.HtmlUtils(),
+ html = '<table><tr><td>Foo</td><td><a>bar</a></td><td style="display: none">baz</td></tr></table>';
+
+ assert.strictEqual( utils.htmlToText( html ), 'Foo bar', 'works' );
+ } );
+
+ QUnit.test( 'htmlToTextWithLinks()', function ( assert ) {
+ var utils = new mw.mmv.HtmlUtils(),
+ html = '<table><tr><td><b>F</b>o<i>o</i></td><td><a>bar</a></td><td style="display: none">baz</td></tr></table>';
+
+ assert.strictEqual( utils.htmlToTextWithLinks( html ), 'Foo <a>bar</a>', 'works' );
+ } );
+
+ QUnit.test( 'htmlToTextWithTags()', function ( assert ) {
+ var utils = new mw.mmv.HtmlUtils(),
+ html = '<table><tr><td><b>F</b>o<i>o</i><sub>o</sub><sup>o</sup></td><td><a>bar</a></td><td style="display: none">baz</td></tr></table>';
+
+ assert.strictEqual( utils.htmlToTextWithTags( html ), '<b>F</b>o<i>o</i><sub>o</sub><sup>o</sup> <a>bar</a>', 'works' );
+ } );
+
+ QUnit.test( 'isJQueryOrHTMLElement()', function ( assert ) {
+ var utils = new mw.mmv.HtmlUtils();
+
+ assert.ok( utils.isJQueryOrHTMLElement( $( '<span>' ) ), 'Recognizes jQuery objects correctly' );
+ assert.ok( utils.isJQueryOrHTMLElement( $( '<span>' ).get( 0 ) ), 'Recognizes HTMLElements correctly' );
+ assert.ok( !utils.isJQueryOrHTMLElement( '<span></span>' ), 'Recognizes jQuery objects correctly' );
+ } );
+
+ QUnit.test( 'makeLinkText()', function ( assert ) {
+ var utils = new mw.mmv.HtmlUtils();
+
+ assert.strictEqual( utils.makeLinkText( 'foo', {
+ href: 'http://example.com',
+ title: 'h<b>t</b><i>m</i>l'
+ } ), '<a href="http://example.com" title="html">foo</a>', 'works' );
+ } );
+}( mediaWiki, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.ThumbnailWidthCalculator.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.ThumbnailWidthCalculator.test.js
new file mode 100644
index 00000000..33484e42
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.ThumbnailWidthCalculator.test.js
@@ -0,0 +1,149 @@
+( function ( mw ) {
+ QUnit.module( 'mmv.ThumbnailWidthCalculator', QUnit.newMwEnvironment() );
+
+ QUnit.test( 'ThumbnailWidthCalculator constructor sanity check', function ( assert ) {
+ var badWidthBuckets = [],
+ goodWidthBuckets = [ 1 ],
+ thumbnailWidthCalculator;
+
+ thumbnailWidthCalculator = new mw.mmv.ThumbnailWidthCalculator();
+ assert.ok( thumbnailWidthCalculator, 'constructor with no argument works' );
+
+ thumbnailWidthCalculator = new mw.mmv.ThumbnailWidthCalculator( {} );
+ assert.ok( thumbnailWidthCalculator, 'constructor with empty option argument works' );
+
+ thumbnailWidthCalculator = new mw.mmv.ThumbnailWidthCalculator( {
+ widthBuckets: goodWidthBuckets
+ } );
+ assert.ok( thumbnailWidthCalculator, 'constructor with non-default buckets works' );
+
+ try {
+ thumbnailWidthCalculator = new mw.mmv.ThumbnailWidthCalculator( {
+ widthBuckets: badWidthBuckets
+ } );
+ } catch ( e ) {
+ assert.ok( e, 'constructor with empty bucket list throws exception' );
+ }
+ } );
+
+ QUnit.test( 'findNextBucket() test', function ( assert ) {
+ var thumbnailWidthCalculator = new mw.mmv.ThumbnailWidthCalculator( {
+ widthBuckets: [ 100, 200 ]
+ } );
+
+ assert.strictEqual( thumbnailWidthCalculator.findNextBucket( 50 ), 100,
+ 'return first bucket for value smaller than all buckets' );
+
+ assert.strictEqual( thumbnailWidthCalculator.findNextBucket( 300 ), 200,
+ 'return last bucket for value larger than all buckets' );
+
+ assert.strictEqual( thumbnailWidthCalculator.findNextBucket( 150 ), 200,
+ 'return next bucket for value between two buckets' );
+
+ assert.strictEqual( thumbnailWidthCalculator.findNextBucket( 100 ), 100,
+ 'return bucket for value equal to that bucket' );
+ } );
+
+ // Old tests for the default bucket sizes. Preserved because why not.
+ QUnit.test( 'We get sane image sizes when we ask for them', function ( assert ) {
+ var twc = new mw.mmv.ThumbnailWidthCalculator();
+
+ assert.strictEqual( twc.findNextBucket( 200 ), 320, 'Low target size gives us lowest possible size bucket' );
+ assert.strictEqual( twc.findNextBucket( 320 ), 320, 'Asking for a bucket size gives us exactly that bucket size' );
+ assert.strictEqual( twc.findNextBucket( 320.00001 ), 800, 'Asking for greater than an image bucket definitely gives us the next size up' );
+ assert.strictEqual( twc.findNextBucket( 2000 ), 2560, 'The image bucketing also works on big screens' );
+ assert.strictEqual( twc.findNextBucket( 3000 ), 2880, 'The image bucketing also works on REALLY big screens' );
+ } );
+
+ QUnit.test( 'findNextBucket() test with unordered bucket list', function ( assert ) {
+ var thumbnailWidthCalculator = new mw.mmv.ThumbnailWidthCalculator( {
+ widthBuckets: [ 200, 100 ]
+ } );
+
+ assert.strictEqual( thumbnailWidthCalculator.findNextBucket( 50 ), 100,
+ 'return first bucket for value smaller than all buckets' );
+
+ assert.strictEqual( thumbnailWidthCalculator.findNextBucket( 300 ), 200,
+ 'return last bucket for value larger than all buckets' );
+
+ assert.strictEqual( thumbnailWidthCalculator.findNextBucket( 150 ), 200,
+ 'return next bucket for value between two buckets' );
+ } );
+
+ QUnit.test( 'calculateFittingWidth() test', function ( assert ) {
+ var boundingWidth = 100,
+ boundingHeight = 200,
+ thumbnailWidthCalculator = new mw.mmv.ThumbnailWidthCalculator( { widthBuckets: [ 1 ] } );
+
+ // 50x10 image in 100x200 box - need to scale up 2x
+ assert.strictEqual(
+ thumbnailWidthCalculator.calculateFittingWidth( boundingWidth, boundingHeight, 50, 10 ),
+ 100, 'fit calculation correct when limited by width' );
+
+ // 10x100 image in 100x200 box - need to scale up 2x
+ assert.strictEqual(
+ thumbnailWidthCalculator.calculateFittingWidth( boundingWidth, boundingHeight, 10, 100 ),
+ 20, 'fit calculation correct when limited by height' );
+
+ // 10x20 image in 100x200 box - need to scale up 10x
+ assert.strictEqual(
+ thumbnailWidthCalculator.calculateFittingWidth( boundingWidth, boundingHeight, 10, 20 ),
+ 100, 'fit calculation correct when same aspect ratio' );
+ } );
+
+ QUnit.test( 'calculateWidths() test', function ( assert ) {
+ var boundingWidth = 100,
+ boundingHeight = 200,
+ thumbnailWidthCalculator = new mw.mmv.ThumbnailWidthCalculator( {
+ widthBuckets: [ 8, 16, 32, 64, 128, 256, 512 ],
+ devicePixelRatio: 1
+ } ),
+ widths;
+
+ // 50x10 image in 100x200 box - image size should be 100x20, thumbnail should be 128x25.6
+ widths = thumbnailWidthCalculator.calculateWidths( boundingWidth, boundingHeight, 50, 10 );
+ assert.strictEqual( widths.cssWidth, 100, 'css width is correct when limited by width' );
+ assert.strictEqual( widths.cssHeight, 20, 'css height is correct when limited by width' );
+ assert.strictEqual( widths.real, 128, 'real width is correct when limited by width' );
+
+ // 10x100 image in 100x200 box - image size should be 20x200, thumbnail should be 32x320
+ widths = thumbnailWidthCalculator.calculateWidths( boundingWidth, boundingHeight, 10, 100 );
+ assert.strictEqual( widths.cssWidth, 20, 'css width is correct when limited by height' );
+ assert.strictEqual( widths.cssHeight, 200, 'css height is correct when limited by width' );
+ assert.strictEqual( widths.real, 32, 'real width is correct when limited by height' );
+
+ // 10x20 image in 100x200 box - image size should be 100x200, thumbnail should be 128x256
+ widths = thumbnailWidthCalculator.calculateWidths( boundingWidth, boundingHeight, 10, 20 );
+ assert.strictEqual( widths.cssWidth, 100, 'css width is correct when same aspect ratio' );
+ assert.strictEqual( widths.cssHeight, 200, 'css height is correct when limited by width' );
+ assert.strictEqual( widths.real, 128, 'real width is correct when same aspect ratio' );
+ } );
+
+ QUnit.test( 'calculateWidths() test with non-standard device pixel ratio', function ( assert ) {
+ var boundingWidth = 100,
+ boundingHeight = 200,
+ thumbnailWidthCalculator = new mw.mmv.ThumbnailWidthCalculator( {
+ widthBuckets: [ 8, 16, 32, 64, 128, 256, 512 ],
+ devicePixelRatio: 2
+ } ),
+ widths;
+
+ // 50x10 image in 100x200 box - image size should be 100x20, thumbnail should be 256x51.2
+ widths = thumbnailWidthCalculator.calculateWidths( boundingWidth, boundingHeight, 50, 10 );
+ assert.strictEqual( widths.cssWidth, 100, 'css width is correct when limited by width' );
+ assert.strictEqual( widths.cssHeight, 20, 'css height is correct when limited by width' );
+ assert.strictEqual( widths.real, 256, 'real width is correct when limited by width' );
+
+ // 10x100 image in 100x200 box - image size should be 20x200, thumbnail should be 64x640
+ widths = thumbnailWidthCalculator.calculateWidths( boundingWidth, boundingHeight, 10, 100 );
+ assert.strictEqual( widths.cssWidth, 20, 'css width is correct when limited by height' );
+ assert.strictEqual( widths.cssHeight, 200, 'css height is correct when limited by width' );
+ assert.strictEqual( widths.real, 64, 'real width is correct when limited by height' );
+
+ // 10x20 image in 100x200 box - image size should be 100x200, thumbnail should be 256x512
+ widths = thumbnailWidthCalculator.calculateWidths( boundingWidth, boundingHeight, 10, 20 );
+ assert.strictEqual( widths.cssWidth, 100, 'css width is correct when same aspect ratio' );
+ assert.strictEqual( widths.cssHeight, 200, 'css height is correct when limited by width' );
+ assert.strictEqual( widths.real, 256, 'real width is correct when same aspect ratio' );
+ } );
+}( mediaWiki ) );
diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.bootstrap.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.bootstrap.test.js
new file mode 100644
index 00000000..793f2b52
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.bootstrap.test.js
@@ -0,0 +1,582 @@
+( function ( mw, $ ) {
+ QUnit.module( 'mmv.bootstrap', QUnit.newMwEnvironment( {
+ setup: function () {
+ mw.config.set( 'wgMediaViewer', true );
+ mw.config.set( 'wgMediaViewerOnClick', true );
+ this.sandbox.stub( mw.user, 'isAnon' ).returns( false );
+ }
+ } ) );
+
+ function createGallery( imageSrc, caption ) {
+ var $div = $( '<div>' ).addClass( 'gallery' ).appendTo( '#qunit-fixture' ),
+ $galleryBox = $( '<div>' ).addClass( 'gallerybox' ).appendTo( $div ),
+ $thumbwrap = $( '<div>' ).addClass( 'thumb' ).appendTo( $galleryBox ),
+ $link = $( '<a>' ).addClass( 'image' ).appendTo( $thumbwrap );
+
+ $( '<img>' ).attr( 'src', ( imageSrc || 'thumb.jpg' ) ).appendTo( $link );
+ $( '<div>' ).addClass( 'gallerytext' ).text( caption || 'Foobar' ).appendTo( $galleryBox );
+
+ return $div;
+ }
+
+ function createThumb( imageSrc, caption, alt ) {
+ var $div = $( '<div>' ).addClass( 'thumb' ).appendTo( '#qunit-fixture' ),
+ $link = $( '<a>' ).addClass( 'image' ).appendTo( $div );
+
+ $( '<div>' ).addClass( 'thumbcaption' ).appendTo( $div ).text( caption );
+ $( '<img>' ).attr( 'src', ( imageSrc || 'thumb.jpg' ) ).attr( 'alt', alt ).appendTo( $link );
+
+ return $div;
+ }
+
+ function createNormal( imageSrc, caption ) {
+ var $link = $( '<a>' ).prop( 'title', caption ).addClass( 'image' ).appendTo( '#qunit-fixture' );
+ $( '<img>' ).prop( 'src', ( imageSrc || 'thumb.jpg' ) ).appendTo( $link );
+ return $link;
+ }
+
+ function createMultipleImage( images ) {
+ var i, $div, $thumbimage, $link,
+ $contain = $( '<div>' ).addClass( 'thumb' ),
+ $thumbinner = $( '<div>' ).addClass( 'thumbinner' ).appendTo( $contain );
+ for ( i = 0; i < images.length; ++i ) {
+ $div = $( '<div>' ).appendTo( $thumbinner );
+ $thumbimage = $( '<div>' ).addClass( 'thumbimage' ).appendTo( $div );
+ $link = $( '<a>' ).addClass( 'image' ).appendTo( $thumbimage );
+ $( '<img>' ).prop( 'src', images[ i ][ 0 ] ).appendTo( $link );
+ $( '<div>' ).addClass( 'thumbcaption' ).text( images[ i ][ 1 ] ).appendTo( $div );
+ }
+ return $contain;
+ }
+
+ function createBootstrap( viewer ) {
+ var bootstrap = new mw.mmv.MultimediaViewerBootstrap();
+
+ bootstrap.processThumbs( $( '#qunit-fixture' ) );
+
+ // MultimediaViewerBootstrap.ensureEventHandlersAreSetUp() is a weird workaround for gadget bugs.
+ // MediaViewer should work without it, and so should the tests.
+ bootstrap.ensureEventHandlersAreSetUp = $.noop;
+
+ bootstrap.getViewer = function () {
+ return viewer || { initWithThumbs: $.noop, hash: $.noop };
+ };
+
+ return bootstrap;
+ }
+
+ function hashTest( prefix, bootstrap, assert ) {
+ var hash = prefix + '/foo',
+ callCount = 0;
+
+ bootstrap.loadViewer = function () {
+ callCount++;
+ return $.Deferred().reject();
+ };
+
+ // Hijack loadViewer, which will return a promise that we'll have to
+ // wait for if we want to see these tests through
+ mw.mmv.testHelpers.asyncMethod( bootstrap, 'loadViewer' );
+
+ bootstrap.setupEventHandlers();
+
+ // invalid hash, should not trigger MMV load
+ window.location.hash = 'Foo';
+
+ // actual hash we want to test for, should trigger MMV load
+ // use setTimeout to add new hash change to end of the call stack,
+ // ensuring that event handlers for our previous change can execute
+ // without us interfering with another immediate change
+ setTimeout( function () {
+ window.location.hash = hash;
+ } );
+
+ return mw.mmv.testHelpers.waitForAsync().then( function () {
+ assert.ok( callCount === 1, 'Viewer should be loaded once' );
+ bootstrap.cleanupEventHandlers();
+ window.location.hash = '';
+ } );
+ }
+
+ QUnit.test( 'Promise does not hang on ResourceLoader errors', function ( assert ) {
+ var bootstrap,
+ errorMessage = 'loading failed',
+ done = assert.async();
+
+ this.sandbox.stub( mw.loader, 'using' )
+ .callsArgWith( 2, new Error( errorMessage, [ 'mmv' ] ) )
+ .withArgs( 'mediawiki.notification' ).returns( $.Deferred().reject() ); // needed for mw.notify
+
+ bootstrap = createBootstrap();
+ this.sandbox.stub( bootstrap, 'setupOverlay' );
+ this.sandbox.stub( bootstrap, 'cleanupOverlay' );
+
+ bootstrap.loadViewer( true ).fail( function ( message ) {
+ assert.ok( bootstrap.setupOverlay.called, 'Overlay was set up' );
+ assert.ok( bootstrap.cleanupOverlay.called, 'Overlay was cleaned up' );
+ assert.strictEqual( message, errorMessage, 'promise is rejected with the error message when loading fails' );
+ done();
+ } );
+ } );
+
+ QUnit.test( 'Clicks are not captured once the loading fails', function ( assert ) {
+ var event, returnValue,
+ bootstrap = new mw.mmv.MultimediaViewerBootstrap(),
+ clock = this.sandbox.useFakeTimers();
+
+ this.sandbox.stub( mw.loader, 'using' )
+ .callsArgWith( 2, new Error( 'loading failed', [ 'mmv' ] ) )
+ .withArgs( 'mediawiki.notification' ).returns( $.Deferred().reject() ); // needed for mw.notify
+ bootstrap.ensureEventHandlersAreSetUp = $.noop;
+
+ // trigger first click, which will cause MMV to be loaded (which we've
+ // set up to fail)
+ event = new $.Event( 'click', { button: 0, which: 1 } );
+ returnValue = bootstrap.click( {}, event, 'foo' );
+ clock.tick( 10 );
+ assert.ok( event.isDefaultPrevented(), 'First click is caught' );
+ assert.strictEqual( returnValue, false, 'First click is caught' );
+
+ // wait until MMW is loaded (or failed to load, in this case) before we
+ // trigger another click - which should then not be caught
+ event = new $.Event( 'click', { button: 0, which: 1 } );
+ returnValue = bootstrap.click( {}, event, 'foo' );
+ clock.tick( 10 );
+ assert.ok( !event.isDefaultPrevented(), 'Click after loading failure is not caught' );
+ assert.notStrictEqual( returnValue, false, 'Click after loading failure is not caught' );
+
+ clock.restore();
+ } );
+
+ /* FIXME: Tests suspended as they do not pass in QUnit 2.x+ – T192932
+ QUnit.test( 'Check viewer invoked when clicking on valid image links', function ( assert ) {
+ // TODO: Is <div class="gallery"><span class="image"><img/></span></div> valid ???
+ var div, link, link2, link3, link4, link5, bootstrap,
+ viewer = { initWithThumbs: $.noop, loadImageByTitle: this.sandbox.stub() },
+ clock = this.sandbox.useFakeTimers();
+
+ // Create gallery with valid link image
+ div = createGallery();
+ link = div.find( 'a.image' );
+
+ // Valid isolated thumbnail
+ link2 = $( '<a>' ).addClass( 'image' ).appendTo( '#qunit-fixture' );
+ $( '<img>' ).attr( 'src', 'thumb2.jpg' ).appendTo( link2 );
+
+ // Non-valid fragment
+ link3 = $( '<a>' ).addClass( 'noImage' ).appendTo( div );
+ $( '<img>' ).attr( 'src', 'thumb3.jpg' ).appendTo( link3 );
+
+ mw.config.set( 'wgTitle', 'Thumb4.jpg' );
+ mw.config.set( 'wgNamespaceNumber', 6 );
+ $( '<div>' ).addClass( 'fullMedia' ).appendTo( div );
+ $( '<img>' ).attr( 'src', 'thumb4.jpg' ).appendTo(
+ $( '<a>' )
+ .appendTo(
+ $( '<div>' )
+ .attr( 'id', 'file' )
+ .appendTo( '#qunit-fixture' )
+ )
+ );
+
+ // Create a new bootstrap object to trigger the DOM scan, etc.
+ bootstrap = createBootstrap( viewer );
+ this.sandbox.stub( bootstrap, 'setupOverlay' );
+
+ link4 = $( '.fullMedia .mw-mmv-view-expanded' );
+ assert.ok( link4.length, 'Link for viewing expanded file was set up.' );
+
+ link5 = $( '.fullMedia .mw-mmv-view-config' );
+ assert.ok( link5.length, 'Link for opening enable/disable configuration was set up.' );
+
+ // Click on valid link
+ link.trigger( { type: 'click', which: 1 } );
+ clock.tick( 10 );
+ // FIXME: Actual bootstrap.setupOverlay.callCount: 2
+ assert.equal( bootstrap.setupOverlay.callCount, 1, 'setupOverlay called (1st click)' );
+ assert.equal( viewer.loadImageByTitle.callCount, 1, 'loadImageByTitle called (1st click)' );
+ this.sandbox.reset();
+
+ // Click on valid link
+ link2.trigger( { type: 'click', which: 1 } );
+ clock.tick( 10 );
+ assert.equal( bootstrap.setupOverlay.callCount, 1, 'setupOverlay called (2nd click)' );
+ assert.equal( viewer.loadImageByTitle.callCount, 1, 'loadImageByTitle called (2nd click)' );
+ this.sandbox.reset();
+
+ // Click on valid link
+ link4.trigger( { type: 'click', which: 1 } );
+ clock.tick( 10 );
+ assert.equal( bootstrap.setupOverlay.callCount, 1, 'setupOverlay called (3rd click)' );
+ assert.equal( viewer.loadImageByTitle.callCount, 1, 'loadImageByTitle called (3rd click)' );
+ this.sandbox.reset();
+
+ // Click on valid link even when preference says not to
+ mw.config.set( 'wgMediaViewerOnClick', false );
+ link4.trigger( { type: 'click', which: 1 } );
+ clock.tick( 10 );
+ mw.config.set( 'wgMediaViewerOnClick', true );
+ assert.equal( bootstrap.setupOverlay.callCount, 1, 'setupOverlay called on-click with pref off' );
+ assert.equal( viewer.loadImageByTitle.callCount, 1, 'loadImageByTitle called on-click with pref off' );
+ this.sandbox.reset();
+
+ // @todo comment that above clicks should result in call, below clicks should not
+
+ // Click on non-valid link
+ link3.trigger( { type: 'click', which: 1 } );
+ clock.tick( 10 );
+ assert.equal( bootstrap.setupOverlay.callCount, 0, 'setupOverlay not called on non-valid link click' );
+ assert.equal( viewer.loadImageByTitle.callCount, 0, 'loadImageByTitle not called on non-valid link click' );
+ this.sandbox.reset();
+
+ // Click on valid links with preference off
+ mw.config.set( 'wgMediaViewerOnClick', false );
+ link.trigger( { type: 'click', which: 1 } );
+ link2.trigger( { type: 'click', which: 1 } );
+ clock.tick( 10 );
+ assert.equal( bootstrap.setupOverlay.callCount, 0, 'setupOverlay not called on non-valid link click with pref off' );
+ assert.equal( viewer.loadImageByTitle.callCount, 0, 'loadImageByTitle not called on non-valid link click with pref off' );
+
+ clock.restore();
+ } );
+ */
+
+ QUnit.test( 'Skip images with invalid extensions', function ( assert ) {
+ var div, link,
+ viewer = { initWithThumbs: $.noop, loadImageByTitle: this.sandbox.stub() },
+ clock = this.sandbox.useFakeTimers();
+
+ // Create gallery with image that has invalid name extension
+ div = createGallery( 'thumb.badext' );
+ link = div.find( 'a.image' );
+
+ // Create a new bootstrap object to trigger the DOM scan, etc.
+ createBootstrap( viewer );
+
+ // Click on valid link with wrong image extension
+ link.trigger( { type: 'click', which: 1 } );
+ clock.tick( 10 );
+
+ assert.ok( !viewer.loadImageByTitle.called, 'Image should not be loaded' );
+
+ clock.restore();
+ } );
+
+ /* FIXME: Tests suspended as they do not pass in QUnit 2.x+ – T192932
+ QUnit.test( 'Accept only left clicks without modifier keys, skip the rest', function ( assert ) {
+ var $div, $link, bootstrap,
+ viewer = { initWithThumbs: $.noop, loadImageByTitle: this.sandbox.stub() },
+ clock = this.sandbox.useFakeTimers();
+
+ // Create gallery with image that has valid name extension
+ $div = createGallery();
+
+ // Create a new bootstrap object to trigger the DOM scan, etc.
+ bootstrap = createBootstrap( viewer );
+ this.sandbox.stub( bootstrap, 'setupOverlay' );
+
+ $link = $div.find( 'a.image' );
+
+ // Handle valid left click, it should try to load the image
+ $link.trigger( { type: 'click', which: 1 } );
+ clock.tick( 10 );
+
+ // FIXME: Actual bootstrap.setupOverlay.callCount: 2
+ assert.equal( bootstrap.setupOverlay.callCount, 1, 'Left-click: Set up overlay' );
+ assert.equal( viewer.loadImageByTitle.callCount, 1, 'Left-click: Load image' );
+ this.sandbox.reset();
+
+ // Skip Ctrl-left-click, no image is loaded
+ $link.trigger( { type: 'click', which: 1, ctrlKey: true } );
+ clock.tick( 10 );
+ assert.equal( bootstrap.setupOverlay.callCount, 0, 'Ctrl-left-click: No overlay' );
+ assert.equal( viewer.loadImageByTitle.callCount, 0, 'Ctrl-left-click: No image load' );
+ this.sandbox.reset();
+
+ // Skip invalid right click, no image is loaded
+ $link.trigger( { type: 'click', which: 2 } );
+ clock.tick( 10 );
+ assert.equal( bootstrap.setupOverlay.callCount, 0, 'Right-click: No overlay' );
+ assert.equal( viewer.loadImageByTitle.callCount, 0, 'Right-click: Image was not loaded' );
+
+ clock.restore();
+ } );
+ */
+
+ QUnit.test( 'Ensure that the correct title is loaded when clicking', function ( assert ) {
+ var bootstrap,
+ viewer = { initWithThumbs: $.noop, loadImageByTitle: this.sandbox.stub() },
+ $div = createGallery( 'foo.jpg' ),
+ $link = $div.find( 'a.image' ),
+ clock = this.sandbox.useFakeTimers();
+
+ // Create a new bootstrap object to trigger the DOM scan, etc.
+ bootstrap = createBootstrap( viewer );
+ this.sandbox.stub( bootstrap, 'setupOverlay' );
+
+ $link.trigger( { type: 'click', which: 1 } );
+ clock.tick( 10 );
+ assert.ok( bootstrap.setupOverlay.called, 'Overlay was set up' );
+ assert.strictEqual( viewer.loadImageByTitle.firstCall.args[ 0 ].getPrefixedDb(), 'File:Foo.jpg', 'Titles are identical' );
+
+ clock.restore();
+ } );
+
+ /* FIXME: Tests suspended as they do not pass in QUnit 2.x+ – T192932
+ QUnit.test( 'Validate new LightboxImage object has sane constructor parameters', function ( assert ) {
+ var bootstrap,
+ $div,
+ $link,
+ viewer = mw.mmv.testHelpers.getMultimediaViewer(),
+ fname = 'valid',
+ imgSrc = '/' + fname + '.jpg/300px-' + fname + '.jpg',
+ imgRegex = new RegExp( imgSrc + '$' ),
+ clock = this.sandbox.useFakeTimers();
+
+ $div = createThumb( imgSrc, 'Blah blah', 'meow' );
+ $link = $div.find( 'a.image' );
+
+ // Create a new bootstrap object to trigger the DOM scan, etc.
+ bootstrap = createBootstrap( viewer );
+ this.sandbox.stub( bootstrap, 'setupOverlay' );
+ this.sandbox.stub( viewer, 'createNewImage' );
+ viewer.loadImage = $.noop;
+ viewer.createNewImage = function ( fileLink, filePageLink, fileTitle, index, thumb, caption, alt ) {
+ var html = thumb.outerHTML;
+
+ // FIXME: fileLink doesn't match imgRegex (null)
+ assert.ok( fileLink.match( imgRegex ), 'Thumbnail URL used in creating new image object' );
+ assert.strictEqual( filePageLink, '', 'File page link is sane when creating new image object' );
+ assert.strictEqual( fileTitle.title, fname, 'Filename is correct when passed into new image constructor' );
+ assert.strictEqual( index, 0, 'The only image we created in the gallery is set at index 0 in the images array' );
+ assert.ok( html.indexOf( ' src="' + imgSrc + '"' ) > 0, 'The image element passed in contains the src=... we want.' );
+ assert.ok( html.indexOf( ' alt="meow"' ) > 0, 'The image element passed in contains the alt=... we want.' );
+ assert.strictEqual( caption, 'Blah blah', 'The caption passed in is correct' );
+ assert.strictEqual( alt, 'meow', 'The alt text passed in is correct' );
+ };
+
+ $link.trigger( { type: 'click', which: 1 } );
+ clock.tick( 10 );
+ assert.equal( bootstrap.setupOverlay.callCount, 1, 'Overlay was set up' );
+
+ clock.reset();
+ } );
+ */
+
+ QUnit.test( 'Only load the viewer on a valid hash (modern browsers)', function ( assert ) {
+ var bootstrap;
+
+ window.location.hash = '';
+
+ bootstrap = createBootstrap();
+
+ return hashTest( '/media', bootstrap, assert );
+ } );
+
+ QUnit.test( 'Only load the viewer on a valid hash (old browsers)', function ( assert ) {
+ var bootstrap;
+
+ window.location.hash = '';
+
+ bootstrap = createBootstrap();
+ bootstrap.browserHistory = undefined;
+
+ return hashTest( '/media', bootstrap, assert );
+ } );
+
+ QUnit.test( 'Load the viewer on a legacy hash (modern browsers)', function ( assert ) {
+ var bootstrap;
+
+ window.location.hash = '';
+
+ bootstrap = createBootstrap();
+
+ return hashTest( 'mediaviewer', bootstrap, assert );
+ } );
+
+ QUnit.test( 'Load the viewer on a legacy hash (old browsers)', function ( assert ) {
+ var bootstrap;
+
+ window.location.hash = '';
+
+ bootstrap = createBootstrap();
+ bootstrap.browserHistory = undefined;
+
+ return hashTest( 'mediaviewer', bootstrap, assert );
+ } );
+
+ QUnit.test( 'Overlay is set up on hash change', function ( assert ) {
+ var bootstrap;
+
+ window.location.hash = '#/media/foo';
+
+ bootstrap = createBootstrap();
+ this.sandbox.stub( bootstrap, 'setupOverlay' );
+
+ bootstrap.hash();
+
+ assert.ok( bootstrap.setupOverlay.called, 'Overlay is set up' );
+ } );
+
+ QUnit.test( 'Overlay is not set up on an irrelevant hash change', function ( assert ) {
+ var bootstrap;
+
+ window.location.hash = '#foo';
+
+ bootstrap = createBootstrap();
+ this.sandbox.stub( bootstrap, 'setupOverlay' );
+ bootstrap.loadViewer();
+ bootstrap.setupOverlay.reset();
+
+ bootstrap.hash();
+
+ assert.ok( !bootstrap.setupOverlay.called, 'Overlay is not set up' );
+ } );
+
+ QUnit.test( 'internalHashChange', function ( assert ) {
+ var bootstrap = createBootstrap(),
+ hash = '#/media/foo',
+ callCount = 0,
+ clock = this.sandbox.useFakeTimers();
+
+ window.location.hash = '';
+
+ bootstrap.loadViewer = function () {
+ callCount++;
+ return $.Deferred().reject();
+ };
+
+ bootstrap.setupEventHandlers();
+
+ bootstrap.internalHashChange( { hash: hash } );
+ clock.tick( 10 );
+
+ assert.ok( callCount === 0, 'Viewer should not be loaded' );
+ assert.strictEqual( window.location.hash, hash, 'Window\'s hash has been updated correctly' );
+
+ bootstrap.cleanupEventHandlers();
+ window.location.hash = '';
+ clock.restore();
+ } );
+
+ QUnit.test( 'internalHashChange (legacy)', function ( assert ) {
+ var bootstrap = createBootstrap(),
+ hash = '#mediaviewer/foo',
+ callCount = 0,
+ clock = this.sandbox.useFakeTimers();
+
+ window.location.hash = '';
+
+ bootstrap.loadViewer = function () {
+ callCount++;
+ return $.Deferred().reject();
+ };
+
+ bootstrap.setupEventHandlers();
+
+ bootstrap.internalHashChange( { hash: hash } );
+ clock.tick( 10 );
+
+ assert.ok( callCount === 0, 'Viewer should not be loaded' );
+ assert.strictEqual( window.location.hash, hash, 'Window\'s hash has been updated correctly' );
+
+ bootstrap.cleanupEventHandlers();
+ window.location.hash = '';
+ clock.restore();
+ } );
+
+ QUnit.test( 'Restoring article scroll position', function ( assert ) {
+ var stubbedScrollTop,
+ bootstrap = createBootstrap(),
+ $window = $( window ),
+ done = assert.async();
+
+ this.sandbox.stub( $.fn, 'scrollTop', function ( scrollTop ) {
+ if ( scrollTop !== undefined ) {
+ stubbedScrollTop = scrollTop;
+ return this;
+ } else {
+ return stubbedScrollTop;
+ }
+ } );
+
+ $window.scrollTop( 50 );
+ bootstrap.setupOverlay();
+ // Calling this a second time because it can happen in history navigation context
+ bootstrap.setupOverlay();
+ // Clear scrollTop to check it is restored
+ $window.scrollTop( 0 );
+ bootstrap.cleanupOverlay();
+
+ // Scroll restoration is on a setTimeout
+ setTimeout( function () {
+ assert.strictEqual( $( window ).scrollTop(), 50, 'Scroll is correctly reset to original top position' );
+ done();
+ } );
+ } );
+
+ QUnit.test( 'Preload JS/CSS dependencies on thumb hover', function ( assert ) {
+ var $div, bootstrap,
+ clock = this.sandbox.useFakeTimers(),
+ viewer = { initWithThumbs: $.noop };
+
+ // Create gallery with image that has valid name extension
+ $div = createThumb();
+
+ // Create a new bootstrap object to trigger the DOM scan, etc.
+ bootstrap = createBootstrap( viewer );
+
+ this.sandbox.stub( mw.loader, 'load' );
+
+ $div.mouseenter();
+ clock.tick( bootstrap.hoverWaitDuration - 50 );
+ $div.mouseleave();
+
+ assert.ok( !mw.loader.load.called, 'Dependencies should not be preloaded if the thumb is not hovered long enough' );
+
+ $div.mouseenter();
+ clock.tick( bootstrap.hoverWaitDuration + 50 );
+ $div.mouseleave();
+
+ assert.ok( mw.loader.load.called, 'Dependencies should be preloaded if the thumb is hovered long enough' );
+
+ clock.restore();
+ } );
+
+ QUnit.test( 'isAllowedThumb', function ( assert ) {
+ var $container = $( '<div>' ),
+ $thumb = $( '<img>' ).appendTo( $container ),
+ bootstrap = createBootstrap();
+
+ assert.ok( bootstrap.isAllowedThumb( $thumb ), 'Normal image in a div is allowed.' );
+
+ $container.addClass( 'metadata' );
+ assert.strictEqual( bootstrap.isAllowedThumb( $thumb ), false, 'Image in a metadata container is disallowed.' );
+
+ $container.prop( 'class', '' );
+ $container.addClass( 'noviewer' );
+ assert.strictEqual( bootstrap.isAllowedThumb( $thumb ), false, 'Image in a noviewer container is disallowed.' );
+
+ $container.prop( 'class', '' );
+ $container.addClass( 'noarticletext' );
+ assert.strictEqual( bootstrap.isAllowedThumb( $thumb ), false, 'Image in an empty article is disallowed.' );
+
+ $container.prop( 'class', '' );
+ $thumb.addClass( 'noviewer' );
+ assert.strictEqual( bootstrap.isAllowedThumb( $thumb ), false, 'Image with a noviewer class is disallowed.' );
+ } );
+
+ QUnit.test( 'findCaption', function ( assert ) {
+ var gallery = createGallery( 'foo.jpg', 'Baz' ),
+ thumb = createThumb( 'foo.jpg', 'Quuuuux' ),
+ link = createNormal( 'foo.jpg', 'Foobar' ),
+ multiple = createMultipleImage( [ [ 'foo.jpg', 'Image #1' ], [ 'bar.jpg', 'Image #2' ],
+ [ 'foobar.jpg', 'Image #3' ] ] ),
+ bootstrap = createBootstrap();
+
+ assert.strictEqual( bootstrap.findCaption( gallery.find( '.thumb' ), gallery.find( 'a.image' ) ), 'Baz', 'A gallery caption is found.' );
+ assert.strictEqual( bootstrap.findCaption( thumb, thumb.find( 'a.image' ) ), 'Quuuuux', 'A thumbnail caption is found.' );
+ assert.strictEqual( bootstrap.findCaption( $(), link ), 'Foobar', 'The caption is found even if the image is not a thumbnail.' );
+ assert.strictEqual( bootstrap.findCaption( multiple, multiple.find( 'img[src="bar.jpg"]' ).closest( 'a' ) ), 'Image #2', 'The caption is found in {{Multiple image}}.' );
+ } );
+}( mediaWiki, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.lightboximage.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.lightboximage.test.js
new file mode 100644
index 00000000..ce824707
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.lightboximage.test.js
@@ -0,0 +1,10 @@
+( function ( mw ) {
+ QUnit.module( 'mmv.lightboximage', QUnit.newMwEnvironment() );
+
+ QUnit.test( 'Sanity test, object creation', function ( assert ) {
+ var lightboxImage = new mw.mmv.LightboxImage( 'foo.png' );
+
+ assert.ok( lightboxImage, 'Object created !' );
+ } );
+
+}( mediaWiki, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.lightboxinterface.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.lightboxinterface.test.js
new file mode 100644
index 00000000..1f85eeca
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.lightboxinterface.test.js
@@ -0,0 +1,306 @@
+( function ( mw, $ ) {
+ var oldScrollTo;
+
+ function stubScrollTo() {
+ oldScrollTo = $.scrollTo;
+ $.scrollTo = function () { return { scrollTop: $.noop, on: $.noop, off: $.noop }; };
+ }
+
+ function restoreScrollTo() {
+ $.scrollTo = oldScrollTo;
+ }
+
+ QUnit.module( 'mmv.lightboxInterface', QUnit.newMwEnvironment( {
+ setup: function () {
+ // animation would keep running, conflict with other tests
+ this.sandbox.stub( $.fn, 'animate' ).returnsThis();
+ }
+ } ) );
+
+ QUnit.test( 'Sanity test, object creation and ui construction', function ( assert ) {
+ var lightbox = new mw.mmv.LightboxInterface();
+
+ stubScrollTo();
+
+ function checkIfUIAreasAttachedToDocument( inDocument ) {
+ var msg = ( inDocument === 1 ? ' ' : ' not ' ) + 'attached.';
+ assert.strictEqual( $( '.mw-mmv-wrapper' ).length, inDocument, 'Wrapper area' + msg );
+ assert.strictEqual( $( '.mw-mmv-main' ).length, inDocument, 'Main area' + msg );
+ assert.strictEqual( $( '.mw-mmv-title' ).length, inDocument, 'Title area' + msg );
+ assert.strictEqual( $( '.mw-mmv-credit' ).length, inDocument, 'Author/source area' + msg );
+ assert.strictEqual( $( '.mw-mmv-image-desc' ).length, inDocument, 'Description area' + msg );
+ assert.strictEqual( $( '.mw-mmv-image-links' ).length, inDocument, 'Links area' + msg );
+ }
+
+ // UI areas not attached to the document yet.
+ checkIfUIAreasAttachedToDocument( 0 );
+
+ // Attach lightbox to testing fixture to avoid interference with other tests.
+ lightbox.attach( '#qunit-fixture' );
+
+ // UI areas should now be attached to the document.
+ checkIfUIAreasAttachedToDocument( 1 );
+
+ // Check that the close button on the lightbox still follow the spec (being visible right away)
+ assert.strictEqual( $( '#qunit-fixture .mw-mmv-close' ).length, 1, 'There should be a close button' );
+ assert.ok( $( '#qunit-fixture .mw-mmv-close' ).is( ':visible' ), 'The close button should be visible' );
+
+ // Unattach lightbox from document
+ lightbox.unattach();
+
+ // UI areas not attached to the document anymore.
+ checkIfUIAreasAttachedToDocument( 0 );
+
+ restoreScrollTo();
+ } );
+
+ QUnit.test( 'Handler registration and clearance work OK', function ( assert ) {
+ var lightbox = new mw.mmv.LightboxInterface(),
+ handlerCalls = 0,
+ clock = this.sandbox.useFakeTimers();
+
+ function handleEvent() {
+ handlerCalls++;
+ }
+
+ lightbox.handleEvent( 'test', handleEvent );
+ $( document ).trigger( 'test' );
+ clock.tick( 10 );
+ assert.strictEqual( handlerCalls, 1, 'The handler was called when we triggered the event.' );
+
+ lightbox.clearEvents();
+
+ $( document ).trigger( 'test' );
+ clock.tick( 10 );
+ assert.strictEqual( handlerCalls, 1, 'The handler was not called after calling lightbox.clearEvents().' );
+
+ clock.restore();
+ } );
+
+ QUnit.test( 'Fullscreen mode', function ( assert ) {
+ var lightbox = new mw.mmv.LightboxInterface(),
+ oldFnEnterFullscreen = $.fn.enterFullscreen,
+ oldFnExitFullscreen = $.fn.exitFullscreen,
+ oldSupportFullscreen = $.support.fullscreen;
+
+ // Since we don't want these tests to really open fullscreen
+ // which is subject to user security confirmation,
+ // we use a mock that pretends regular jquery.fullscreen behavior happened
+ $.fn.enterFullscreen = mw.mmv.testHelpers.enterFullscreenMock;
+ $.fn.exitFullscreen = mw.mmv.testHelpers.exitFullscreenMock;
+
+ stubScrollTo();
+
+ lightbox.buttons.fadeOut = $.noop;
+
+ // Attach lightbox to testing fixture to avoid interference with other tests.
+ lightbox.attach( '#qunit-fixture' );
+
+ $.support.fullscreen = false;
+ lightbox.setupCanvasButtons();
+
+ assert.strictEqual( lightbox.$fullscreenButton.css( 'display' ), 'none',
+ 'Fullscreen button is hidden when fullscreen mode is unavailable' );
+
+ $.support.fullscreen = true;
+ lightbox.setupCanvasButtons();
+
+ assert.strictEqual( lightbox.$fullscreenButton.css( 'display' ), '',
+ 'Fullscreen button is visible when fullscreen mode is available' );
+
+ // Entering fullscreen
+ lightbox.$fullscreenButton.click();
+
+ assert.strictEqual( lightbox.$main.hasClass( 'jq-fullscreened' ), true,
+ 'Fullscreened area has the fullscreen class' );
+ assert.strictEqual( lightbox.isFullscreen, true, 'Lightbox knows it\'s in fullscreen mode' );
+
+ // Exiting fullscreen
+ lightbox.$fullscreenButton.click();
+
+ assert.strictEqual( lightbox.$main.hasClass( 'jq-fullscreened' ), false,
+ 'Fullscreened area doesn\'t have the fullscreen class anymore' );
+ assert.strictEqual( lightbox.isFullscreen, false, 'Lightbox knows it\'s not in fullscreen mode' );
+
+ // Entering fullscreen
+ lightbox.$fullscreenButton.click();
+
+ // Hard-exiting fullscreen
+ lightbox.$closeButton.click();
+
+ // Re-attach after hard-exit
+ lightbox.attach( '#qunit-fixture' );
+
+ assert.strictEqual( lightbox.$main.hasClass( 'jq-fullscreened' ), false,
+ 'Fullscreened area doesn\'t have the fullscreen class anymore' );
+ assert.strictEqual( lightbox.isFullscreen, false, 'Lightbox knows it\'s not in fullscreen mode' );
+
+ // Unattach lightbox from document
+ lightbox.unattach();
+
+ $.fn.enterFullscreen = oldFnEnterFullscreen;
+ $.fn.exitFullscreen = oldFnExitFullscreen;
+ $.support.fullscreen = oldSupportFullscreen;
+ restoreScrollTo();
+ } );
+
+ QUnit.test( 'Fullscreen mode', function ( assert ) {
+ var buttonOffset, panelBottom,
+ oldRevealButtonsAndFadeIfNeeded,
+ lightbox = new mw.mmv.LightboxInterface(),
+ viewer = mw.mmv.testHelpers.getMultimediaViewer(),
+ oldFnEnterFullscreen = $.fn.enterFullscreen,
+ oldFnExitFullscreen = $.fn.exitFullscreen;
+
+ stubScrollTo();
+
+ // ugly hack to avoid preloading which would require lightbox list being set up
+ viewer.preloadDistance = -1;
+
+ // Since we don't want these tests to really open fullscreen
+ // which is subject to user security confirmation,
+ // we use a mock that pretends regular jquery.fullscreen behavior happened
+ $.fn.enterFullscreen = mw.mmv.testHelpers.enterFullscreenMock;
+ $.fn.exitFullscreen = mw.mmv.testHelpers.exitFullscreenMock;
+
+ // Attach lightbox to testing fixture to avoid interference with other tests.
+ lightbox.attach( '#qunit-fixture' );
+ viewer.ui = lightbox;
+ viewer.ui = lightbox;
+
+ assert.ok( !lightbox.isFullscreen, 'Lightbox knows that it\'s not in fullscreen mode' );
+ assert.ok( lightbox.panel.$imageMetadata.is( ':visible' ), 'Image metadata is visible' );
+
+ lightbox.buttons.fadeOut = function () {
+ assert.ok( true, 'Opening fullscreen triggers a fadeout' );
+ };
+
+ // Pretend that the mouse cursor is on top of the button
+ buttonOffset = lightbox.buttons.$fullscreen.offset();
+ lightbox.mousePosition = { x: buttonOffset.left, y: buttonOffset.top };
+
+ // Enter fullscreen
+ lightbox.buttons.$fullscreen.click();
+
+ lightbox.buttons.fadeOut = $.noop;
+ assert.ok( lightbox.isFullscreen, 'Lightbox knows that it\'s in fullscreen mode' );
+
+ oldRevealButtonsAndFadeIfNeeded = lightbox.buttons.revealAndFade;
+
+ lightbox.buttons.revealAndFade = function ( position ) {
+ assert.ok( true, 'Moving the cursor triggers a reveal + fade' );
+
+ oldRevealButtonsAndFadeIfNeeded.call( this, position );
+ };
+
+ // Pretend that the mouse cursor moved to the top-left corner
+ lightbox.mousemove( { pageX: 0, pageY: 0 } );
+
+ lightbox.buttons.revealAndFadeIfNeeded = $.noop;
+
+ panelBottom = $( '.mw-mmv-post-image' ).position().top + $( '.mw-mmv-post-image' ).height();
+
+ assert.ok( panelBottom === $( window ).height(), 'Image metadata does not extend beyond the viewport' );
+
+ lightbox.buttons.revealAndFade = function ( position ) {
+ assert.ok( true, 'Closing fullscreen triggers a reveal + fade' );
+
+ oldRevealButtonsAndFadeIfNeeded.call( this, position );
+ };
+
+ // Exiting fullscreen
+ lightbox.buttons.$fullscreen.click();
+
+ panelBottom = $( '.mw-mmv-post-image' ).position().top + $( '.mw-mmv-post-image' ).height();
+
+ assert.ok( panelBottom > $( window ).height(), 'Image metadata extends beyond the viewport' );
+ assert.ok( !lightbox.isFullscreen, 'Lightbox knows that it\'s not in fullscreen mode' );
+
+ // Unattach lightbox from document
+ lightbox.unattach();
+
+ $.fn.enterFullscreen = oldFnEnterFullscreen;
+ $.fn.exitFullscreen = oldFnExitFullscreen;
+ restoreScrollTo();
+ } );
+
+ QUnit.test( 'isAnyActiveButtonHovered', function ( assert ) {
+ var lightbox = new mw.mmv.LightboxInterface();
+
+ stubScrollTo();
+
+ // Attach lightbox to testing fixture to avoid interference with other tests.
+ lightbox.attach( '#qunit-fixture' );
+
+ $.each( lightbox.buttons.$buttons, function ( idx, e ) {
+ var $e = $( e ),
+ offset = $e.show().offset(),
+ width = $e.width(),
+ height = $e.height(),
+ disabled = $e.hasClass( 'disabled' );
+
+ assert.strictEqual( lightbox.buttons.isAnyActiveButtonHovered( offset.left, offset.top ),
+ !disabled,
+ 'Hover detection works for top-left corner of element' );
+ assert.strictEqual( lightbox.buttons.isAnyActiveButtonHovered( offset.left + width, offset.top ),
+ !disabled,
+ 'Hover detection works for top-right corner of element' );
+ assert.strictEqual( lightbox.buttons.isAnyActiveButtonHovered( offset.left, offset.top + height ),
+ !disabled,
+ 'Hover detection works for bottom-left corner of element' );
+ assert.strictEqual( lightbox.buttons.isAnyActiveButtonHovered( offset.left + width, offset.top + height ),
+ !disabled,
+ 'Hover detection works for bottom-right corner of element' );
+ assert.strictEqual(
+ lightbox.buttons.isAnyActiveButtonHovered(
+ offset.left + ( width / 2 ), offset.top + ( height / 2 )
+ ),
+ !disabled,
+ 'Hover detection works for center of element'
+ );
+ } );
+
+ // Unattach lightbox from document
+ lightbox.unattach();
+ restoreScrollTo();
+ } );
+
+ QUnit.test( 'Keyboard prev/next', function ( assert ) {
+ var viewer = mw.mmv.testHelpers.getMultimediaViewer(),
+ lightbox = new mw.mmv.LightboxInterface();
+
+ viewer.setupEventHandlers();
+
+ // Since we define both, the test works regardless of RTL settings
+ lightbox.on( 'next', function () {
+ assert.ok( true, 'Next image was open' );
+ } );
+
+ lightbox.on( 'prev', function () {
+ assert.ok( true, 'Prev image was open' );
+ } );
+
+ // 37 is left arrow, 39 is right arrow
+ lightbox.keydown( $.Event( 'keydown', { which: 37 } ) );
+ lightbox.keydown( $.Event( 'keydown', { which: 39 } ) );
+
+ lightbox.off( 'next' ).on( 'next', function () {
+ assert.ok( false, 'Next image should not have been open' );
+ } );
+
+ lightbox.off( 'prev' ).on( 'prev', function () {
+ assert.ok( false, 'Prev image should not have been open' );
+ } );
+
+ lightbox.keydown( $.Event( 'keydown', { which: 37, altKey: true } ) );
+ lightbox.keydown( $.Event( 'keydown', { which: 39, altKey: true } ) );
+ lightbox.keydown( $.Event( 'keydown', { which: 37, ctrlKey: true } ) );
+ lightbox.keydown( $.Event( 'keydown', { which: 39, ctrlKey: true } ) );
+ lightbox.keydown( $.Event( 'keydown', { which: 37, shiftKey: true } ) );
+ lightbox.keydown( $.Event( 'keydown', { which: 39, shiftKey: true } ) );
+ lightbox.keydown( $.Event( 'keydown', { which: 37, metaKey: true } ) );
+ lightbox.keydown( $.Event( 'keydown', { which: 39, metaKey: true } ) );
+
+ viewer.cleanupEventHandlers();
+ } );
+}( mediaWiki, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.test.js
new file mode 100644
index 00000000..95a01c36
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.test.js
@@ -0,0 +1,706 @@
+( function ( mw, $ ) {
+ QUnit.module( 'mmv', QUnit.newMwEnvironment() );
+
+ QUnit.test( 'eachPrealoadableLightboxIndex()', function ( assert ) {
+ var viewer = mw.mmv.testHelpers.getMultimediaViewer(),
+ expectedIndices,
+ i;
+
+ viewer.preloadDistance = 3;
+ viewer.thumbs = [];
+
+ // 0..10
+ for ( i = 0; i < 11; i++ ) {
+ viewer.thumbs.push( { image: false } );
+ }
+
+ viewer.currentIndex = 2;
+ i = 0;
+ expectedIndices = [ 2, 3, 1, 4, 0, 5 ];
+ viewer.eachPrealoadableLightboxIndex( function ( index ) {
+ assert.strictEqual( index, expectedIndices[ i++ ], 'preload on left edge' );
+ } );
+
+ viewer.currentIndex = 9;
+ i = 0;
+ expectedIndices = [ 9, 10, 8, 7, 6 ];
+ viewer.eachPrealoadableLightboxIndex( function ( index ) {
+ assert.strictEqual( index, expectedIndices[ i++ ], 'preload on right edge' );
+ } );
+ } );
+
+ QUnit.test( 'Hash handling', function ( assert ) {
+ var oldUnattach,
+ viewer = mw.mmv.testHelpers.getMultimediaViewer(),
+ ui = new mw.mmv.LightboxInterface(),
+ imageSrc = 'Foo bar.jpg',
+ image = { filePageTitle: new mw.Title( 'File:' + imageSrc ) };
+
+ // animation would keep running, conflict with other tests
+ this.sandbox.stub( $.fn, 'animate' ).returnsThis();
+
+ window.location.hash = '';
+
+ viewer.setupEventHandlers();
+ oldUnattach = ui.unattach;
+
+ ui.unattach = function () {
+ assert.ok( true, 'Lightbox was unattached' );
+ oldUnattach.call( this );
+ };
+
+ viewer.ui = ui;
+ viewer.close();
+
+ assert.ok( !viewer.isOpen, 'Viewer is closed' );
+
+ viewer.isOpen = true;
+
+ // Verify that passing an invalid mmv hash when the mmv is open triggers unattach()
+ window.location.hash = 'Foo';
+ viewer.hash();
+
+ // Verify that mmv doesn't reset a foreign hash
+ assert.strictEqual( window.location.hash, '#Foo', 'Foreign hash remains intact' );
+ assert.ok( !viewer.isOpen, 'Viewer is closed' );
+
+ ui.unattach = function () {
+ assert.ok( false, 'Lightbox was not unattached' );
+ oldUnattach.call( this );
+ };
+
+ // Verify that passing an invalid mmv hash when the mmv is closed doesn't trigger unattach()
+ window.location.hash = 'Bar';
+ viewer.hash();
+
+ // Verify that mmv doesn't reset a foreign hash
+ assert.strictEqual( window.location.hash, '#Bar', 'Foreign hash remains intact' );
+
+ viewer.ui = { images: [ image ], disconnect: $.noop };
+
+ $( '#qunit-fixture' ).append( '<a class="image"><img src="' + imageSrc + '"></a>' );
+
+ viewer.loadImageByTitle = function ( title ) {
+ assert.strictEqual( title.getPrefixedText(), 'File:' + imageSrc, 'The title matches' );
+ };
+
+ // Open a valid mmv hash link and check that the right image is requested.
+ // imageSrc contains a space without any encoding on purpose
+ window.location.hash = '/media/File:' + imageSrc;
+ viewer.hash();
+
+ // Reset the hash, because for some browsers switching from the non-URI-encoded to
+ // the non-URI-encoded version of the same text with a space will not trigger a hash change
+ window.location.hash = '';
+ viewer.hash();
+
+ // Try again with an URI-encoded imageSrc containing a space
+ window.location.hash = '/media/File:' + encodeURIComponent( imageSrc );
+ viewer.hash();
+
+ // Reset the hash
+ window.location.hash = '';
+ viewer.hash();
+
+ // Try again with a legacy hash
+ window.location.hash = 'mediaviewer/File:' + imageSrc;
+ viewer.hash();
+
+ viewer.cleanupEventHandlers();
+
+ window.location.hash = '';
+ } );
+
+ QUnit.test( 'Progress', function ( assert ) {
+ var imageDeferred = $.Deferred(),
+ viewer = mw.mmv.testHelpers.getMultimediaViewer(),
+ fakeImage = {
+ filePageTitle: new mw.Title( 'File:Stuff.jpg' ),
+ extraStatsDeferred: $.Deferred().reject()
+ },
+ // custom clock ensures progress handlers execute in correct sequence
+ clock = this.sandbox.useFakeTimers();
+
+ viewer.thumbs = [];
+ viewer.displayPlaceholderThumbnail = $.noop;
+ viewer.setImage = $.noop;
+ viewer.scroll = $.noop;
+ viewer.preloadFullscreenThumbnail = $.noop;
+ viewer.fetchSizeIndependentLightboxInfo = function () { return $.Deferred().resolve( {} ); };
+ viewer.ui = {
+ setFileReuseData: $.noop,
+ setupForLoad: $.noop,
+ canvas: { set: $.noop,
+ unblurWithAnimation: $.noop,
+ unblur: $.noop,
+ getCurrentImageWidths: function () { return { real: 0 }; },
+ getDimensions: function () { return {}; }
+ },
+ panel: {
+ setImageInfo: $.noop,
+ scroller: {
+ animateMetadataOnce: $.noop
+ },
+ progressBar: {
+ animateTo: this.sandbox.stub(),
+ jumpTo: this.sandbox.stub()
+ }
+ },
+ open: $.noop };
+
+ viewer.imageProvider.get = function () { return imageDeferred.promise(); };
+ viewer.imageInfoProvider.get = function () { return $.Deferred().resolve( {} ); };
+ viewer.thumbnailInfoProvider.get = function () { return $.Deferred().resolve( {} ); };
+
+ // loadImage will call setupProgressBar, which will attach done, fail &
+ // progress handlers
+ viewer.loadImage( fakeImage, new Image() );
+ clock.tick( 10 );
+ assert.ok( viewer.ui.panel.progressBar.jumpTo.lastCall.calledWith( 0 ),
+ 'Percentage correctly reset by loadImage' );
+ assert.ok( viewer.ui.panel.progressBar.animateTo.firstCall.calledWith( 5 ),
+ 'Percentage correctly animated to 5 by loadImage' );
+
+ imageDeferred.notify( 'response', 45 );
+ clock.tick( 10 );
+ assert.ok( viewer.ui.panel.progressBar.animateTo.secondCall.calledWith( 45 ),
+ 'Percentage correctly funneled to panel UI' );
+
+ imageDeferred.resolve( {}, {} );
+ clock.tick( 10 );
+ assert.ok( viewer.ui.panel.progressBar.animateTo.thirdCall.calledWith( 100 ),
+ 'Percentage correctly funneled to panel UI' );
+
+ clock.restore();
+
+ viewer.close();
+ } );
+
+ QUnit.test( 'Progress when switching images', function ( assert ) {
+ var firstImageDeferred = $.Deferred(),
+ secondImageDeferred = $.Deferred(),
+ firstImage = {
+ index: 1,
+ filePageTitle: new mw.Title( 'File:First.jpg' ),
+ extraStatsDeferred: $.Deferred().reject()
+ },
+ secondImage = {
+ index: 2,
+ filePageTitle: new mw.Title( 'File:Second.jpg' ),
+ extraStatsDeferred: $.Deferred().reject()
+ },
+ viewer = mw.mmv.testHelpers.getMultimediaViewer(),
+ // custom clock ensures progress handlers execute in correct sequence
+ clock = this.sandbox.useFakeTimers();
+
+ // animation would keep running, conflict with other tests
+ this.sandbox.stub( $.fn, 'animate' ).returnsThis();
+
+ viewer.thumbs = [];
+ viewer.displayPlaceholderThumbnail = $.noop;
+ viewer.setImage = $.noop;
+ viewer.scroll = $.noop;
+ viewer.preloadFullscreenThumbnail = $.noop;
+ viewer.preloadImagesMetadata = $.noop;
+ viewer.preloadThumbnails = $.noop;
+ viewer.fetchSizeIndependentLightboxInfo = function () { return $.Deferred().resolve( {} ); };
+ viewer.ui = {
+ setFileReuseData: $.noop,
+ setupForLoad: $.noop,
+ canvas: { set: $.noop,
+ unblurWithAnimation: $.noop,
+ unblur: $.noop,
+ getCurrentImageWidths: function () { return { real: 0 }; },
+ getDimensions: function () { return {}; }
+ },
+ panel: {
+ setImageInfo: $.noop,
+ scroller: {
+ animateMetadataOnce: $.noop
+ },
+ progressBar: {
+ hide: this.sandbox.stub(),
+ animateTo: this.sandbox.stub(),
+ jumpTo: this.sandbox.stub()
+ }
+ },
+ open: $.noop,
+ empty: $.noop };
+
+ viewer.imageInfoProvider.get = function () { return $.Deferred().resolve( {} ); };
+ viewer.thumbnailInfoProvider.get = function () { return $.Deferred().resolve( {} ); };
+
+ // load some image
+ viewer.imageProvider.get = this.sandbox.stub().returns( firstImageDeferred );
+ viewer.loadImage( firstImage, new Image() );
+ clock.tick( 10 );
+ assert.ok( viewer.ui.panel.progressBar.jumpTo.getCall( 0 ).calledWith( 0 ),
+ 'Percentage correctly reset for new first image' );
+ assert.ok( viewer.ui.panel.progressBar.animateTo.getCall( 0 ).calledWith( 5 ),
+ 'Percentage correctly animated to 5 for first new image' );
+
+ // progress on active image
+ firstImageDeferred.notify( 'response', 20 );
+ clock.tick( 10 );
+ assert.ok( viewer.ui.panel.progressBar.animateTo.getCall( 1 ).calledWith( 20 ),
+ 'Percentage correctly animated when active image is loading' );
+
+ // change to another image
+ viewer.imageProvider.get = this.sandbox.stub().returns( secondImageDeferred );
+ viewer.loadImage( secondImage, new Image() );
+ clock.tick( 10 );
+ assert.ok( viewer.ui.panel.progressBar.jumpTo.getCall( 1 ).calledWith( 0 ),
+ 'Percentage correctly reset for second new image' );
+ assert.ok( viewer.ui.panel.progressBar.animateTo.getCall( 2 ).calledWith( 5 ),
+ 'Percentage correctly animated to 5 for second new image' );
+
+ // progress on active image
+ secondImageDeferred.notify( 'response', 30 );
+ clock.tick( 10 );
+ assert.ok( viewer.ui.panel.progressBar.animateTo.getCall( 3 ).calledWith( 30 ),
+ 'Percentage correctly animated when active image is loading' );
+
+ // progress on inactive image
+ firstImageDeferred.notify( 'response', 40 );
+ clock.tick( 10 );
+ assert.ok( viewer.ui.panel.progressBar.animateTo.callCount === 4,
+ 'Percentage not animated when inactive image is loading' );
+
+ // progress on active image
+ secondImageDeferred.notify( 'response', 50 );
+ clock.tick( 10 );
+ assert.ok( viewer.ui.panel.progressBar.animateTo.getCall( 4 ).calledWith( 50 ),
+ 'Percentage correctly ignored inactive image & only animated when active image is loading' );
+
+ // change back to first image
+ viewer.imageProvider.get = this.sandbox.stub().returns( firstImageDeferred );
+ viewer.loadImage( firstImage, new Image() );
+ clock.tick( 10 );
+ assert.ok( viewer.ui.panel.progressBar.jumpTo.getCall( 2 ).calledWith( 40 ),
+ 'Percentage jumps to right value when changing images' );
+
+ secondImageDeferred.resolve( {}, {} );
+ clock.tick( 10 );
+ assert.ok( !viewer.ui.panel.progressBar.hide.called,
+ 'Progress bar not hidden when something finishes in the background' );
+
+ // change back to second image, which has finished loading
+ viewer.imageProvider.get = this.sandbox.stub().returns( secondImageDeferred );
+ viewer.loadImage( secondImage, new Image() );
+ clock.tick( 10 );
+ assert.ok( viewer.ui.panel.progressBar.hide.called,
+ 'Progress bar hidden when switching to finished image' );
+
+ clock.restore();
+
+ viewer.close();
+ } );
+
+ QUnit.test( 'resetBlurredThumbnailStates', function ( assert ) {
+ var viewer = mw.mmv.testHelpers.getMultimediaViewer();
+
+ // animation would keep running, conflict with other tests
+ this.sandbox.stub( $.fn, 'animate' ).returnsThis();
+
+ assert.ok( !viewer.realThumbnailShown, 'Real thumbnail state is correct' );
+ assert.ok( !viewer.blurredThumbnailShown, 'Placeholder state is correct' );
+
+ viewer.realThumbnailShown = true;
+ viewer.blurredThumbnailShown = true;
+
+ viewer.resetBlurredThumbnailStates();
+
+ assert.ok( !viewer.realThumbnailShown, 'Real thumbnail state is correct' );
+ assert.ok( !viewer.blurredThumbnailShown, 'Placeholder state is correct' );
+ } );
+
+ QUnit.test( 'Placeholder first, then real thumbnail', function ( assert ) {
+ var viewer = mw.mmv.testHelpers.getMultimediaViewer();
+
+ viewer.setImage = $.noop;
+ viewer.ui = { canvas: {
+ unblurWithAnimation: $.noop,
+ unblur: $.noop,
+ maybeDisplayPlaceholder: function () { return true; }
+ } };
+ viewer.imageInfoProvider.get = this.sandbox.stub();
+
+ viewer.displayPlaceholderThumbnail( { originalWidth: 100, originalHeight: 100 }, undefined, undefined );
+
+ assert.ok( viewer.blurredThumbnailShown, 'Placeholder state is correct' );
+ assert.ok( !viewer.realThumbnailShown, 'Real thumbnail state is correct' );
+
+ viewer.displayRealThumbnail( { url: undefined } );
+
+ assert.ok( viewer.realThumbnailShown, 'Real thumbnail state is correct' );
+ assert.ok( viewer.blurredThumbnailShown, 'Placeholder state is correct' );
+ } );
+
+ QUnit.test( 'Placeholder first, then real thumbnail - missing size', function ( assert ) {
+ var viewer = mw.mmv.testHelpers.getMultimediaViewer();
+
+ viewer.currentIndex = 1;
+ viewer.setImage = $.noop;
+ viewer.ui = { canvas: {
+ unblurWithAnimation: $.noop,
+ unblur: $.noop,
+ maybeDisplayPlaceholder: function () { return true; }
+ } };
+ viewer.imageInfoProvider.get = this.sandbox.stub().returns( $.Deferred().resolve( { width: 100, height: 100 } ) );
+
+ viewer.displayPlaceholderThumbnail( { index: 1 }, undefined, undefined );
+
+ assert.ok( viewer.blurredThumbnailShown, 'Placeholder state is correct' );
+ assert.ok( !viewer.realThumbnailShown, 'Real thumbnail state is correct' );
+
+ viewer.displayRealThumbnail( { url: undefined } );
+
+ assert.ok( viewer.realThumbnailShown, 'Real thumbnail state is correct' );
+ assert.ok( viewer.blurredThumbnailShown, 'Placeholder state is correct' );
+ } );
+
+ QUnit.test( 'Real thumbnail first, then placeholder', function ( assert ) {
+ var viewer = mw.mmv.testHelpers.getMultimediaViewer();
+
+ viewer.setImage = $.noop;
+ viewer.ui = {
+ showImage: $.noop,
+ canvas: {
+ unblurWithAnimation: $.noop,
+ unblur: $.noop
+ } };
+
+ viewer.displayRealThumbnail( { url: undefined } );
+
+ assert.ok( viewer.realThumbnailShown, 'Real thumbnail state is correct' );
+ assert.ok( !viewer.blurredThumbnailShown, 'Placeholder state is correct' );
+
+ viewer.displayPlaceholderThumbnail( {}, undefined, undefined );
+
+ assert.ok( viewer.realThumbnailShown, 'Real thumbnail state is correct' );
+ assert.ok( !viewer.blurredThumbnailShown, 'Placeholder state is correct' );
+ } );
+
+ QUnit.test( 'displayRealThumbnail', function ( assert ) {
+ var viewer = mw.mmv.testHelpers.getMultimediaViewer();
+
+ viewer.setImage = $.noop;
+ viewer.ui = { canvas: {
+ unblurWithAnimation: this.sandbox.stub(),
+ unblur: $.noop
+ } };
+ viewer.blurredThumbnailShown = true;
+
+ // Should not result in an unblurWithAnimation animation (image cache from cache)
+ viewer.displayRealThumbnail( { url: undefined }, undefined, undefined, 5 );
+ assert.ok( !viewer.ui.canvas.unblurWithAnimation.called, 'There should not be an unblurWithAnimation animation' );
+
+ // Should result in an unblurWithAnimation (image didn't come from cache)
+ viewer.displayRealThumbnail( { url: undefined }, undefined, undefined, 1000 );
+ assert.ok( viewer.ui.canvas.unblurWithAnimation.called, 'There should be an unblurWithAnimation animation' );
+ } );
+
+ QUnit.test( 'New image loaded while another one is loading', function ( assert ) {
+ var viewer = mw.mmv.testHelpers.getMultimediaViewer(),
+ firstImageDeferred = $.Deferred(),
+ secondImageDeferred = $.Deferred(),
+ firstLigthboxInfoDeferred = $.Deferred(),
+ secondLigthboxInfoDeferred = $.Deferred(),
+ firstImage = {
+ filePageTitle: new mw.Title( 'File:Foo.jpg' ),
+ index: 0,
+ extraStatsDeferred: $.Deferred().reject()
+ },
+ secondImage = {
+ filePageTitle: new mw.Title( 'File:Bar.jpg' ),
+ index: 1,
+ extraStatsDeferred: $.Deferred().reject()
+ },
+ // custom clock ensures progress handlers execute in correct sequence
+ clock = this.sandbox.useFakeTimers();
+
+ viewer.preloadFullscreenThumbnail = $.noop;
+ viewer.fetchSizeIndependentLightboxInfo = this.sandbox.stub();
+ viewer.ui = {
+ setFileReuseData: $.noop,
+ setupForLoad: $.noop,
+ canvas: {
+ set: $.noop,
+ getCurrentImageWidths: function () { return { real: 0 }; },
+ getDimensions: function () { return {}; }
+ },
+ panel: {
+ setImageInfo: this.sandbox.stub(),
+ scroller: {
+ animateMetadataOnce: $.noop
+ },
+ progressBar: {
+ animateTo: this.sandbox.stub(),
+ jumpTo: this.sandbox.stub()
+ },
+ empty: $.noop
+ },
+ open: $.noop,
+ empty: $.noop };
+ viewer.displayRealThumbnail = this.sandbox.stub();
+ viewer.eachPrealoadableLightboxIndex = $.noop;
+ viewer.animateMetadataDivOnce = this.sandbox.stub().returns( $.Deferred().reject() );
+ viewer.imageProvider.get = this.sandbox.stub();
+ viewer.imageInfoProvider.get = function () { return $.Deferred().reject(); };
+ viewer.thumbnailInfoProvider.get = function () { return $.Deferred().resolve( {} ); };
+
+ viewer.imageProvider.get.returns( firstImageDeferred.promise() );
+ viewer.fetchSizeIndependentLightboxInfo.returns( firstLigthboxInfoDeferred.promise() );
+ viewer.loadImage( firstImage, new Image() );
+ clock.tick( 10 );
+ assert.ok( !viewer.animateMetadataDivOnce.called, 'Metadata of the first image should not be animated' );
+ assert.ok( !viewer.ui.panel.setImageInfo.called, 'Metadata of the first image should not be shown' );
+
+ viewer.imageProvider.get.returns( secondImageDeferred.promise() );
+ viewer.fetchSizeIndependentLightboxInfo.returns( secondLigthboxInfoDeferred.promise() );
+ viewer.loadImage( secondImage, new Image() );
+ clock.tick( 10 );
+
+ viewer.ui.panel.progressBar.animateTo.reset();
+ firstImageDeferred.notify( undefined, 45 );
+ clock.tick( 10 );
+ assert.ok( !viewer.ui.panel.progressBar.animateTo.reset.called, 'Progress of the first image should not be shown' );
+
+ firstImageDeferred.resolve( {}, {} );
+ firstLigthboxInfoDeferred.resolve( {} );
+ clock.tick( 10 );
+ assert.ok( !viewer.displayRealThumbnail.called, 'The first image being done loading should have no effect' );
+
+ viewer.displayRealThumbnail = this.sandbox.spy( function () { viewer.close(); } );
+ secondImageDeferred.resolve( {}, {} );
+ secondLigthboxInfoDeferred.resolve( {} );
+ clock.tick( 10 );
+ assert.ok( viewer.displayRealThumbnail.called, 'The second image being done loading should result in the image being shown' );
+
+ clock.restore();
+ } );
+
+ QUnit.test( 'Events are not trapped after the viewer is closed', function ( assert ) {
+ var i, j, k, eventParameters,
+ viewer = mw.mmv.testHelpers.getMultimediaViewer(),
+ $document = $( document ),
+ $qf = $( '#qunit-fixture' ),
+ eventTypes = [ 'keydown', 'keyup', 'keypress', 'click', 'mousedown', 'mouseup' ],
+ modifiers = [ undefined, 'altKey', 'ctrlKey', 'shiftKey', 'metaKey' ],
+ // Events are async, we need to wait for the last event to be caught before ending the test
+ done = assert.async(),
+ oldScrollTo = $.scrollTo;
+
+ assert.expect( 0 );
+
+ // animation would keep running, conflict with other tests
+ this.sandbox.stub( $.fn, 'animate' ).returnsThis();
+
+ $.scrollTo = function () { return { scrollTop: $.noop, on: $.noop, off: $.noop }; };
+
+ viewer.setupEventHandlers();
+
+ viewer.imageProvider.get = function () { return $.Deferred().reject(); };
+ viewer.imageInfoProvider.get = function () { return $.Deferred().reject(); };
+ viewer.thumbnailInfoProvider.get = function () { return $.Deferred().reject(); };
+ viewer.fileRepoInfoProvider.get = function () { return $.Deferred().reject(); };
+
+ viewer.preloadFullscreenThumbnail = $.noop;
+ viewer.initWithThumbs( [] );
+
+ viewer.loadImage(
+ {
+ filePageTitle: new mw.Title( 'File:Stuff.jpg' ),
+ thumbnail: new mw.mmv.model.Thumbnail( 'foo', 10, 10 ),
+ extraStatsDeferred: $.Deferred().reject()
+ },
+ new Image()
+ );
+
+ viewer.ui.$closeButton.click();
+
+ function eventHandler( e ) {
+ if ( e.isDefaultPrevented() ) {
+ assert.ok( false, 'Event was incorrectly trapped: ' + e.which );
+ }
+
+ e.preventDefault();
+
+ // Wait for the last event
+ if ( e.which === 32 && e.type === 'mouseup' ) {
+ $document.off( '.mmvtest' );
+ viewer.cleanupEventHandlers();
+ $.scrollTo = oldScrollTo;
+ done();
+ }
+ }
+
+ for ( j = 0; j < eventTypes.length; j++ ) {
+ $document.on( eventTypes[ j ] + '.mmvtest', eventHandler );
+
+ eventloop:
+ for ( i = 0; i < 256; i++ ) {
+ // Save some time by not testing unlikely values for mouse events
+ if ( i > 32 ) {
+ switch ( eventTypes[ j ] ) {
+ case 'click':
+ case 'mousedown':
+ case 'mouseup':
+ break eventloop;
+ }
+ }
+
+ for ( k = 0; k < modifiers.length; k++ ) {
+ eventParameters = { which: i };
+ if ( modifiers[ k ] !== undefined ) {
+ eventParameters[ modifiers[ k ] ] = true;
+ }
+ $qf.trigger( $.Event( eventTypes[ j ], eventParameters ) );
+ }
+ }
+ }
+ } );
+
+ QUnit.test( 'Refuse to load too-big thumbnails', function ( assert ) {
+ var viewer = mw.mmv.testHelpers.getMultimediaViewer(),
+ intendedWidth = 50,
+ title = mw.Title.newFromText( 'File:Foobar.svg' );
+
+ viewer.thumbnailInfoProvider.get = function ( fileTitle, width ) {
+ assert.strictEqual( width, intendedWidth );
+ return $.Deferred().reject();
+ };
+
+ viewer.fetchThumbnail( title, 1000, null, intendedWidth, 60 );
+ } );
+
+ QUnit.test( 'fetchThumbnail()', function ( assert ) {
+ var guessedThumbnailInfoStub,
+ thumbnailInfoStub,
+ imageStub,
+ promise,
+ useThumbnailGuessing,
+ viewer = new mw.mmv.MultimediaViewer( { imageQueryParameter: $.noop, language: $.noop, recordVirtualViewBeaconURI: $.noop, extensions: function () { return { jpg: 'default' }; }, useThumbnailGuessing: function () { return useThumbnailGuessing; } } ),
+ sandbox = this.sandbox,
+ file = new mw.Title( 'File:Copyleft.svg' ),
+ sampleURL = 'http://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/Copyleft.svg/300px-Copyleft.svg.png',
+ width = 100,
+ originalWidth = 1000,
+ originalHeight = 1000,
+ image = {},
+ // custom clock ensures progress handlers execute in correct sequence
+ clock = this.sandbox.useFakeTimers();
+
+ function setupStubs() {
+ guessedThumbnailInfoStub = viewer.guessedThumbnailInfoProvider.get = sandbox.stub();
+ thumbnailInfoStub = viewer.thumbnailInfoProvider.get = sandbox.stub();
+ imageStub = viewer.imageProvider.get = sandbox.stub();
+ }
+
+ useThumbnailGuessing = true;
+
+ // When we lack sample URL and original dimensions, the classic provider should be used
+ setupStubs();
+ guessedThumbnailInfoStub.returns( $.Deferred().resolve( { url: 'guessedURL' } ) );
+ thumbnailInfoStub.returns( $.Deferred().resolve( { url: 'apiURL' } ) );
+ imageStub.returns( $.Deferred().resolve( image ) );
+ promise = viewer.fetchThumbnail( file, width );
+ clock.tick( 10 );
+ assert.ok( !guessedThumbnailInfoStub.called, 'When we lack sample URL and original dimensions, GuessedThumbnailInfoProvider is not called' );
+ assert.ok( thumbnailInfoStub.calledOnce, 'When we lack sample URL and original dimensions, ThumbnailInfoProvider is called once' );
+ assert.ok( imageStub.calledOnce, 'When we lack sample URL and original dimensions, ImageProvider is called once' );
+ assert.ok( imageStub.calledWith( 'apiURL' ), 'When we lack sample URL and original dimensions, ImageProvider is called with the API url' );
+ assert.strictEqual( promise.state(), 'resolved', 'When we lack sample URL and original dimensions, fetchThumbnail resolves' );
+
+ // When the guesser bails out, the classic provider should be used
+ setupStubs();
+ guessedThumbnailInfoStub.returns( $.Deferred().reject() );
+ thumbnailInfoStub.returns( $.Deferred().resolve( { url: 'apiURL' } ) );
+ imageStub.returns( $.Deferred().resolve( image ) );
+ promise = viewer.fetchThumbnail( file, width, sampleURL, originalWidth, originalHeight );
+ clock.tick( 10 );
+ assert.ok( guessedThumbnailInfoStub.calledOnce, 'When the guesser bails out, GuessedThumbnailInfoProvider is called once' );
+ assert.ok( thumbnailInfoStub.calledOnce, 'When the guesser bails out, ThumbnailInfoProvider is called once' );
+ assert.ok( imageStub.calledOnce, 'When the guesser bails out, ImageProvider is called once' );
+ assert.ok( imageStub.calledWith( 'apiURL' ), 'When the guesser bails out, ImageProvider is called with the API url' );
+ assert.strictEqual( promise.state(), 'resolved', 'When the guesser bails out, fetchThumbnail resolves' );
+
+ // When the guesser returns an URL, that should be used
+ setupStubs();
+ guessedThumbnailInfoStub.returns( $.Deferred().resolve( { url: 'guessedURL' } ) );
+ thumbnailInfoStub.returns( $.Deferred().resolve( { url: 'apiURL' } ) );
+ imageStub.returns( $.Deferred().resolve( image ) );
+ promise = viewer.fetchThumbnail( file, width, sampleURL, originalWidth, originalHeight );
+ clock.tick( 10 );
+ assert.ok( guessedThumbnailInfoStub.calledOnce, 'When the guesser returns an URL, GuessedThumbnailInfoProvider is called once' );
+ assert.ok( !thumbnailInfoStub.called, 'When the guesser returns an URL, ThumbnailInfoProvider is not called' );
+ assert.ok( imageStub.calledOnce, 'When the guesser returns an URL, ImageProvider is called once' );
+ assert.ok( imageStub.calledWith( 'guessedURL' ), 'When the guesser returns an URL, ImageProvider is called with the guessed url' );
+ assert.strictEqual( promise.state(), 'resolved', 'When the guesser returns an URL, fetchThumbnail resolves' );
+
+ // When the guesser returns an URL, but that returns 404, image loading should be retried with the classic provider
+ setupStubs();
+ guessedThumbnailInfoStub.returns( $.Deferred().resolve( { url: 'guessedURL' } ) );
+ thumbnailInfoStub.returns( $.Deferred().resolve( { url: 'apiURL' } ) );
+ imageStub.withArgs( 'guessedURL' ).returns( $.Deferred().reject() );
+ imageStub.withArgs( 'apiURL' ).returns( $.Deferred().resolve( image ) );
+ promise = viewer.fetchThumbnail( file, width, sampleURL, originalWidth, originalHeight );
+ clock.tick( 10 );
+ assert.ok( guessedThumbnailInfoStub.calledOnce, 'When the guesser returns an URL, but that returns 404, GuessedThumbnailInfoProvider is called once' );
+ assert.ok( thumbnailInfoStub.calledOnce, 'When the guesser returns an URL, but that returns 404, ThumbnailInfoProvider is called once' );
+ assert.ok( imageStub.calledTwice, 'When the guesser returns an URL, but that returns 404, ImageProvider is called twice' );
+ assert.ok( imageStub.getCall( 0 ).calledWith( 'guessedURL' ), 'When the guesser returns an URL, but that returns 404, ImageProvider is called first with the guessed url' );
+ assert.ok( imageStub.getCall( 1 ).calledWith( 'apiURL' ), 'When the guesser returns an URL, but that returns 404, ImageProvider is called second with the guessed url' );
+ assert.strictEqual( promise.state(), 'resolved', 'When the guesser returns an URL, but that returns 404, fetchThumbnail resolves' );
+
+ // When even the retry fails, fetchThumbnail() should reject
+ setupStubs();
+ guessedThumbnailInfoStub.returns( $.Deferred().resolve( { url: 'guessedURL' } ) );
+ thumbnailInfoStub.returns( $.Deferred().resolve( { url: 'apiURL' } ) );
+ imageStub.withArgs( 'guessedURL' ).returns( $.Deferred().reject() );
+ imageStub.withArgs( 'apiURL' ).returns( $.Deferred().reject() );
+ promise = viewer.fetchThumbnail( file, width, sampleURL, originalWidth, originalHeight );
+ clock.tick( 10 );
+ assert.ok( guessedThumbnailInfoStub.calledOnce, 'When even the retry fails, GuessedThumbnailInfoProvider is called once' );
+ assert.ok( thumbnailInfoStub.calledOnce, 'When even the retry fails, ThumbnailInfoProvider is called once' );
+ assert.ok( imageStub.calledTwice, 'When even the retry fails, ImageProvider is called twice' );
+ assert.ok( imageStub.getCall( 0 ).calledWith( 'guessedURL' ), 'When even the retry fails, ImageProvider is called first with the guessed url' );
+ assert.ok( imageStub.getCall( 1 ).calledWith( 'apiURL' ), 'When even the retry fails, ImageProvider is called second with the guessed url' );
+ assert.strictEqual( promise.state(), 'rejected', 'When even the retry fails, fetchThumbnail rejects' );
+
+ useThumbnailGuessing = false;
+
+ // When guessing is disabled, the classic provider is used
+ setupStubs();
+ guessedThumbnailInfoStub.returns( $.Deferred().resolve( { url: 'guessedURL' } ) );
+ thumbnailInfoStub.returns( $.Deferred().resolve( { url: 'apiURL' } ) );
+ imageStub.returns( $.Deferred().resolve( image ) );
+ promise = viewer.fetchThumbnail( file, width );
+ clock.tick( 10 );
+ assert.ok( !guessedThumbnailInfoStub.called, 'When guessing is disabled, GuessedThumbnailInfoProvider is not called' );
+ assert.ok( thumbnailInfoStub.calledOnce, 'When guessing is disabled, ThumbnailInfoProvider is called once' );
+ assert.ok( imageStub.calledOnce, 'When guessing is disabled, ImageProvider is called once' );
+ assert.ok( imageStub.calledWith( 'apiURL' ), 'When guessing is disabled, ImageProvider is called with the API url' );
+ assert.strictEqual( promise.state(), 'resolved', 'When guessing is disabled, fetchThumbnail resolves' );
+
+ clock.restore();
+ } );
+
+ QUnit.test( 'document.title', function ( assert ) {
+ var viewer = mw.mmv.testHelpers.getMultimediaViewer(),
+ bootstrap = new mw.mmv.MultimediaViewerBootstrap(),
+ title = new mw.Title( 'File:This_should_show_up_in_document_title.png' ),
+ oldDocumentTitle = document.title;
+
+ viewer.currentImageFileTitle = title;
+ bootstrap.setupEventHandlers();
+ viewer.setHash();
+
+ assert.ok( document.title.match( title.getNameText() ), 'File name is visible in title' );
+
+ viewer.close();
+ bootstrap.cleanupEventHandlers();
+
+ assert.strictEqual( document.title, oldDocumentTitle, 'Original title restored after viewer is closed' );
+ } );
+}( mediaWiki, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.testhelpers.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.testhelpers.js
new file mode 100644
index 00000000..4010883a
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/mmv.testhelpers.js
@@ -0,0 +1,174 @@
+( function ( mw, $ ) {
+ var MTH = {};
+
+ MTH.enterFullscreenMock = function () {
+ this.first().addClass( 'jq-fullscreened' ).data( 'isFullscreened', true );
+
+ $( document ).trigger( $.Event( 'jq-fullscreen-change', { element: this, fullscreen: true } ) );
+ };
+
+ MTH.exitFullscreenMock = function () {
+ this.first().removeClass( 'jq-fullscreened' ).data( 'isFullscreened', false );
+
+ $( document ).trigger( $.Event( 'jq-fullscreen-change', { element: this, fullscreen: false } ) );
+ };
+
+ /**
+ * Returns the exception thrown by callback, or undefined if no exception was thrown.
+ *
+ * @param {Function} callback
+ * @return {Error}
+ */
+ MTH.getException = function ( callback ) {
+ var ex;
+ try {
+ callback();
+ } catch ( e ) {
+ ex = e;
+ }
+ return ex;
+ };
+
+ /**
+ * Creates an mw.storage-like object.
+ *
+ * @param {Object} storage localStorage stub with getItem, setItem, removeItem methods
+ * @return {mw.storage} Local storage-like object
+ */
+ MTH.createLocalStorage = function ( storage ) {
+ return new ( Object.getPrototypeOf( mw.storage ) ).constructor( storage );
+ };
+
+ /**
+ * Returns an mw.storage that mimicks lack of localStorage support.
+ *
+ * @return {mw.storage} Local storage-like object
+ */
+ MTH.getUnsupportedLocalStorage = function () {
+ return this.createLocalStorage( undefined );
+ };
+
+ /**
+ * Returns an mw.storage that mimicks localStorage being disabled in browser.
+ *
+ * @return {mw.storage} Local storage-like object
+ */
+ MTH.getDisabledLocalStorage = function () {
+ var e = function () {
+ throw new Error( 'Error' );
+ };
+
+ return this.createLocalStorage( {
+ getItem: e,
+ setItem: e,
+ removeItem: e
+ } );
+ };
+
+ /**
+ * Returns a fake local storage which is not saved between reloads.
+ *
+ * @param {Object} [initialData]
+ * @return {mw.storage} Local storage-like object
+ */
+ MTH.getFakeLocalStorage = function ( initialData ) {
+ var bag = new mw.Map();
+ bag.set( initialData );
+
+ return this.createLocalStorage( {
+ getItem: function ( key ) { return bag.get( key ); },
+ setItem: function ( key, value ) { bag.set( key, value ); },
+ removeItem: function ( key ) { bag.set( key, null ); }
+ } );
+ };
+
+ /**
+ * Returns a viewer object with all the appropriate placeholder functions.
+ *
+ * @return {mv.mmv.MultiMediaViewer} [description]
+ */
+ MTH.getMultimediaViewer = function () {
+ return new mw.mmv.MultimediaViewer( {
+ imageQueryParameter: $.noop,
+ language: $.noop,
+ recordVirtualViewBeaconURI: $.noop,
+ extensions: function () {
+ return { jpg: 'default' };
+ }
+ } );
+ };
+
+ MTH.asyncPromises = [];
+
+ /**
+ * Given a method/function that returns a promise, this'll return a function
+ * that just wraps the original & returns the original result, but also
+ * executes an assert.async() right before it's called, and resolves that
+ * async after that promise has completed.
+ *
+ * Example usage: given a method `bootstrap.openImage` that returns a
+ * promise, just call it like this to wrap this functionality around it:
+ * `bootstrap.openImage = asyncMethod( bootstrap.openImage, bootstrap );`
+ *
+ * Now, every time some part of the code calls this function, it'll just
+ * execute as it normally would, but your tests won't finish until these
+ * functions (and any .then tacked on to them) have completed.
+ *
+ * This method will make sure your tests don't end prematurely (before the
+ * promises have been resolved), but that's it. If you need to run
+ * additional code after all promises have resolved, you can call the
+ * complementary `waitForAsync`, which will return a promise that doesn't
+ * resolve until all of these promises have.
+ *
+ * @param {Object} object
+ * @param {string} method
+ * @param {QUnit.assert} [assert]
+ * @return {Function}
+ */
+ MTH.asyncMethod = function ( object, method, assert ) {
+ return function () {
+ // apply arguments to original promise
+ var promise = object[ method ].apply( object, arguments ),
+ done;
+
+ this.asyncPromises.push( promise );
+
+ if ( assert ) {
+ done = assert.async();
+ // use setTimeout to ensure `done` is not the first callback handler
+ // to execute (possibly ending the test's wait right before
+ // the result of the promise is executed)
+ setTimeout( promise.then.bind( null, done, done ) );
+ }
+
+ return promise;
+ }.bind( this );
+ };
+
+ /**
+ * Returns a promise that will not resolve until all of the promises that
+ * were created in functions upon which `asyncMethod` was called have
+ * resolved.
+ *
+ * @return {$.Promise}
+ */
+ MTH.waitForAsync = function () {
+ var deferred = $.Deferred();
+
+ // it's possible that, before this function call, some code was executed
+ // that triggers async code that will eventually end up `asyncPromises`
+ // in order to give that code a chance to run, we'll add another promise
+ // to the array, that will only resolve at the end of the current call
+ // stack (using setTimeout)
+ this.asyncPromises.push( deferred.promise() );
+ setTimeout( deferred.resolve );
+
+ return QUnit.whenPromisesComplete.apply( null, this.asyncPromises ).then(
+ function () {
+ this.asyncPromises = [];
+ }.bind( this )
+ );
+ };
+
+ mw.mmv.testHelpers = MTH;
+}( mediaWiki, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.EmbedFileInfo.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.EmbedFileInfo.test.js
new file mode 100644
index 00000000..be606300
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.EmbedFileInfo.test.js
@@ -0,0 +1,40 @@
+/*
+ * This file is part of the MediaWiki extension MediaViewer.
+ *
+ * MediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw ) {
+ QUnit.module( 'mmv.model.EmbedFileInfo', QUnit.newMwEnvironment() );
+
+ QUnit.test( 'EmbedFileInfo constructor sanity check', function ( assert ) {
+ var imageInfo = {},
+ repoInfo = {},
+ caption = 'Foo',
+ alt = 'Bar',
+ embedFileInfo = new mw.mmv.model.EmbedFileInfo( imageInfo, repoInfo, caption, alt );
+
+ assert.strictEqual( embedFileInfo.imageInfo, imageInfo, 'ImageInfo is set correctly' );
+ assert.strictEqual( embedFileInfo.repoInfo, repoInfo, 'ImageInfo is set correctly' );
+ assert.strictEqual( embedFileInfo.caption, caption, 'Caption is set correctly' );
+ assert.strictEqual( embedFileInfo.alt, alt, 'Alt text is set correctly' );
+
+ try {
+ embedFileInfo = new mw.mmv.model.EmbedFileInfo( {} );
+ } catch ( e ) {
+ assert.ok( e, 'Exception is thrown when parameters are missing' );
+ }
+ } );
+
+}( mediaWiki ) );
diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.Image.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.Image.test.js
new file mode 100644
index 00000000..f02c55db
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.Image.test.js
@@ -0,0 +1,151 @@
+/*
+ * This file is part of the MediaWiki extension MediaViewer.
+ *
+ * MediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw ) {
+ QUnit.module( 'mmv.model.Image', QUnit.newMwEnvironment() );
+
+ QUnit.test( 'Image model constructor sanity check', function ( assert ) {
+ var
+ title = mw.Title.newFromText( 'File:Foobar.jpg' ),
+ name = 'Foo bar',
+ size = 100,
+ width = 10,
+ height = 15,
+ mime = 'image/jpeg',
+ url = 'https://upload.wikimedia.org/wikipedia/commons/3/3a/Foobar.jpg',
+ pageID = 42,
+ descurl = 'https://commons.wikimedia.org/wiki/File:Foobar.jpg',
+ descShortUrl = '',
+ repo = 'wikimediacommons',
+ datetime = '2011-07-04T23:31:14Z',
+ anondatetime = '20110704000000',
+ origdatetime = '2010-07-04T23:31:14Z',
+ description = 'This is a test file.',
+ source = 'WMF',
+ author = 'Ryan Kaldari',
+ authorCount = 1,
+ permission = 'only use for good, not evil',
+ deletionReason = 'poor quality',
+ license = new mw.mmv.model.License( 'cc0' ),
+ attribution = 'Created by my cats on a winter morning',
+ latitude = 39.12381283,
+ longitude = 100.983829,
+ restrictions = [ 'trademarked' ],
+ imageData = new mw.mmv.model.Image(
+ title, name, size, width, height, mime, url,
+ descurl, descShortUrl, pageID, repo, datetime, anondatetime, origdatetime,
+ description, source, author, authorCount, license, permission, attribution,
+ deletionReason, latitude, longitude, restrictions );
+
+ assert.strictEqual( imageData.title, title, 'Title is set correctly' );
+ assert.strictEqual( imageData.name, name, 'Name is set correctly' );
+ assert.strictEqual( imageData.size, size, 'Size is set correctly' );
+ assert.strictEqual( imageData.width, width, 'Width is set correctly' );
+ assert.strictEqual( imageData.height, height, 'Height is set correctly' );
+ assert.strictEqual( imageData.mimeType, mime, 'MIME type is set correctly' );
+ assert.strictEqual( imageData.url, url, 'URL for original image is set correctly' );
+ assert.strictEqual( imageData.descriptionUrl, descurl, 'URL for image description page is set correctly' );
+ assert.strictEqual( imageData.pageID, pageID, 'Page ID of image description is set correctly' );
+ assert.strictEqual( imageData.repo, repo, 'Repository name is set correctly' );
+ assert.strictEqual( imageData.uploadDateTime, datetime, 'Date and time of last upload is set correctly' );
+ assert.strictEqual( imageData.anonymizedUploadDateTime, anondatetime, 'Anonymized date and time of last upload is set correctly' );
+ assert.strictEqual( imageData.creationDateTime, origdatetime, 'Date and time of original upload is set correctly' );
+ assert.strictEqual( imageData.description, description, 'Description is set correctly' );
+ assert.strictEqual( imageData.source, source, 'Source is set correctly' );
+ assert.strictEqual( imageData.author, author, 'Author is set correctly' );
+ assert.strictEqual( imageData.authorCount, authorCount, 'Author is set correctly' );
+ assert.strictEqual( imageData.license, license, 'License is set correctly' );
+ assert.strictEqual( imageData.permission, permission, 'Permission is set correctly' );
+ assert.strictEqual( imageData.attribution, attribution, 'Attribution is set correctly' );
+ assert.strictEqual( imageData.deletionReason, deletionReason, 'Deletion reason is set correctly' );
+ assert.strictEqual( imageData.latitude, latitude, 'Latitude is set correctly' );
+ assert.strictEqual( imageData.longitude, longitude, 'Longitude is set correctly' );
+ assert.deepEqual( imageData.restrictions, restrictions, 'Restrictions is set correctly' );
+ assert.ok( imageData.thumbUrls, 'Thumb URL cache is set up properly' );
+ } );
+
+ QUnit.test( 'hasCoords()', function ( assert ) {
+ var
+ firstImageData = new mw.mmv.model.Image(
+ mw.Title.newFromText( 'File:Foobar.pdf.jpg' ), 'Foo bar',
+ 10, 10, 10, 'image/jpeg', 'http://example.org', 'http://example.com', 42,
+ 'example', 'tester', '2013-11-10', '20131110', '2013-11-09', 'Blah blah blah',
+ 'A person', 'Another person', 1, 'CC-BY-SA-3.0', 'Permitted', 'My cat'
+ ),
+ secondImageData = new mw.mmv.model.Image(
+ mw.Title.newFromText( 'File:Foobar.pdf.jpg' ), 'Foo bar',
+ 10, 10, 10, 'image/jpeg', 'http://example.org', 'http://example.com', 42,
+ 'example', 'tester', '2013-11-10', '20131110', '2013-11-09', 'Blah blah blah',
+ 'A person', 'Another person', 1, 'CC-BY-SA-3.0', 'Permitted', 'My cat',
+ undefined, '39.91820938', '78.09812938'
+ );
+
+ assert.strictEqual( firstImageData.hasCoords(), false, 'No coordinates present means hasCoords returns false.' );
+ assert.strictEqual( secondImageData.hasCoords(), true, 'Coordinates present means hasCoords returns true.' );
+ } );
+
+ QUnit.test( 'parseExtmeta()', function ( assert ) {
+ var Image = mw.mmv.model.Image,
+ stringData = { value: 'foo' },
+ plaintextData = { value: 'fo<b>o</b>' },
+ integerData = { value: 3 },
+ integerStringData = { value: '3' },
+ zeroPrefixedIntegerStringData = { value: '03' },
+ floatData = { value: 1.23 },
+ floatStringData = { value: '1.23' },
+ booleanData = { value: 'yes' },
+ wrongBooleanData = { value: 'blah' },
+ listDataEmpty = { value: '' },
+ listDataSingle = { value: 'foo' },
+ listDataMultiple = { value: 'foo|bar|baz' },
+ missingData;
+
+ assert.strictEqual( Image.parseExtmeta( stringData, 'string' ), 'foo',
+ 'Extmeta string parsed correctly.' );
+ assert.strictEqual( Image.parseExtmeta( plaintextData, 'plaintext' ), 'foo',
+ 'Extmeta plaintext parsed correctly.' );
+ assert.strictEqual( Image.parseExtmeta( floatData, 'float' ), 1.23,
+ 'Extmeta float parsed correctly.' );
+ assert.strictEqual( Image.parseExtmeta( floatStringData, 'float' ), 1.23,
+ 'Extmeta float string parsed correctly.' );
+ assert.strictEqual( Image.parseExtmeta( booleanData, 'boolean' ), true,
+ 'Extmeta boolean string parsed correctly.' );
+ assert.strictEqual( Image.parseExtmeta( wrongBooleanData, 'boolean' ), undefined,
+ 'Extmeta boolean string with error ignored.' );
+ assert.strictEqual( Image.parseExtmeta( integerData, 'integer' ), 3,
+ 'Extmeta integer parsed correctly.' );
+ assert.strictEqual( Image.parseExtmeta( integerStringData, 'integer' ), 3,
+ 'Extmeta integer string parsed correctly.' );
+ assert.strictEqual( Image.parseExtmeta( zeroPrefixedIntegerStringData, 'integer' ), 3,
+ 'Extmeta zero-prefixed integer string parsed correctly.' );
+ assert.deepEqual( Image.parseExtmeta( listDataEmpty, 'list' ), [],
+ 'Extmeta empty list parsed correctly.' );
+ assert.deepEqual( Image.parseExtmeta( listDataSingle, 'list' ), [ 'foo' ],
+ 'Extmeta list with single element parsed correctly.' );
+ assert.deepEqual( Image.parseExtmeta( listDataMultiple, 'list' ), [ 'foo', 'bar', 'baz' ],
+ 'Extmeta list with multipleelements parsed correctly.' );
+ assert.strictEqual( Image.parseExtmeta( missingData, 'string' ), undefined,
+ 'Extmeta missing data parsed correctly.' );
+
+ try {
+ Image.parseExtmeta( stringData, 'strong' );
+ } catch ( e ) {
+ assert.ok( e, 'Exception is thrown on invalid argument' );
+ }
+ } );
+
+}( mediaWiki ) );
diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.IwTitle.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.IwTitle.test.js
new file mode 100644
index 00000000..27cf119d
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.IwTitle.test.js
@@ -0,0 +1,43 @@
+/*
+ * This file is part of the MediaWiki extension MediaViewer.
+ *
+ * MediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw ) {
+ QUnit.module( 'mmv.model.IwTitle', QUnit.newMwEnvironment() );
+
+ QUnit.test( 'constructor sanity test', function ( assert ) {
+ var namespace = 4,
+ fullPageName = 'User_talk:John_Doe',
+ domain = 'en.wikipedia.org',
+ url = 'https://en.wikipedia.org/wiki/User_talk:John_Doe',
+ title = new mw.mmv.model.IwTitle( namespace, fullPageName, domain, url );
+
+ assert.ok( title );
+ } );
+
+ QUnit.test( 'getters', function ( assert ) {
+ var namespace = 4,
+ fullPageName = 'User_talk:John_Doe',
+ domain = 'en.wikipedia.org',
+ url = 'https://en.wikipedia.org/wiki/User_talk:John_Doe',
+ title = new mw.mmv.model.IwTitle( namespace, fullPageName, domain, url );
+
+ assert.strictEqual( title.getUrl(), url, 'getUrl()' );
+ assert.strictEqual( title.getDomain(), domain, 'getDomain()' );
+ assert.strictEqual( title.getPrefixedDb(), fullPageName, 'getPrefixedDb()' );
+ assert.strictEqual( title.getPrefixedText(), 'User talk:John Doe', 'getPrefixedText()' );
+ } );
+}( mediaWiki ) );
diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.License.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.License.test.js
new file mode 100644
index 00000000..7d758f09
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.License.test.js
@@ -0,0 +1,161 @@
+/*
+ * This file is part of the MediaWiki extension MediaViewer.
+ *
+ * MediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $ ) {
+
+ QUnit.module( 'mmv.model.License', QUnit.newMwEnvironment() );
+
+ QUnit.test( 'License constructor sanity check', function ( assert ) {
+ var license,
+ shortName = 'CC-BY-SA-3.0',
+ internalName = 'cc-by-sa-3.0',
+ longName = 'Creative Commons Attribution--Share-Alike 3.0',
+ url = 'http://creativecommons.org/licenses/by-sa/3.0/';
+
+ license = new mw.mmv.model.License( shortName );
+ assert.ok( license, 'License created successfully' );
+ assert.strictEqual( license.shortName, shortName, 'License has correct short name' );
+ assert.ok( !license.internalName, 'License has no internal name' );
+ assert.ok( !license.longName, 'License has no long name' );
+ assert.ok( !license.deedUrl, 'License has no deed URL' );
+
+ license = new mw.mmv.model.License( shortName, internalName, longName, url );
+ assert.ok( license, 'License created successfully' );
+ assert.strictEqual( license.shortName, shortName, 'License has correct short name' );
+ assert.strictEqual( license.internalName, internalName, 'License has correct internal name' );
+ assert.strictEqual( license.longName, longName, 'License has correct long name' );
+ assert.strictEqual( license.deedUrl, url, 'License has correct deed URL' );
+
+ try {
+ license = new mw.mmv.model.License();
+ } catch ( e ) {
+ assert.ok( e, 'License cannot be created without a short name' );
+ }
+ } );
+
+ QUnit.test( 'getShortName()', function ( assert ) {
+ var existingMessageKey = 'Internal name that does exist',
+ nonExistingMessageKey = 'Internal name that does not exist',
+ license1 = new mw.mmv.model.License( 'Shortname' ),
+ license2 = new mw.mmv.model.License( 'Shortname', nonExistingMessageKey ),
+ license3 = new mw.mmv.model.License( 'Shortname', existingMessageKey ),
+ oldMwMessage = mw.message,
+ oldMwMessagesExists = mw.messages.exists;
+
+ mw.message = function ( name ) {
+ return name === 'multimediaviewer-license-' + existingMessageKey ?
+ { text: function () { return 'Translated name'; } } :
+ oldMwMessage.apply( mw, arguments );
+ };
+ mw.messages.exists = function ( name ) {
+ return name === 'multimediaviewer-license-' + existingMessageKey ?
+ true : oldMwMessagesExists.apply( mw.messages, arguments );
+ };
+
+ assert.strictEqual( license1.getShortName(), 'Shortname',
+ 'Short name is returned when there is no translated name' );
+ assert.strictEqual( license2.getShortName(), 'Shortname',
+ 'Short name is returned when translated name is missing' );
+ assert.strictEqual( license3.getShortName(), 'Translated name',
+ 'Translated name is returned when it exists' );
+
+ mw.message = oldMwMessage;
+ mw.messages.exists = oldMwMessagesExists;
+ } );
+
+ QUnit.test( 'getShortLink()', function ( assert ) {
+ var $html,
+ license1 = new mw.mmv.model.License( 'lorem ipsum' ),
+ license2 = new mw.mmv.model.License( 'lorem ipsum', 'lipsum' ),
+ license3 = new mw.mmv.model.License( 'lorem ipsum', 'lipsum', 'Lorem ipsum dolor sit amet' ),
+ license4 = new mw.mmv.model.License( 'lorem ipsum', 'lipsum', 'Lorem ipsum dolor sit amet',
+ 'http://www.lipsum.com/' );
+
+ assert.strictEqual( license1.getShortLink(), 'lorem ipsum',
+ 'Code for license without link is formatted correctly' );
+ assert.strictEqual( license2.getShortLink(), 'lorem ipsum',
+ 'Code for license without link is formatted correctly' );
+ assert.strictEqual( license3.getShortLink(), 'lorem ipsum',
+ 'Code for license without link is formatted correctly' );
+
+ $html = $( license4.getShortLink() );
+ assert.strictEqual( $html.text(), 'lorem ipsum',
+ 'Text for license with link is formatted correctly' );
+ assert.strictEqual( $html.prop( 'href' ), 'http://www.lipsum.com/',
+ 'URL for license with link is formatted correctly' );
+ assert.strictEqual( $html.prop( 'title' ), 'Lorem ipsum dolor sit amet',
+ 'Title for license with link is formatted correctly' );
+ } );
+
+ QUnit.test( 'isCc()', function ( assert ) {
+ var license;
+
+ license = new mw.mmv.model.License( 'CC-BY-SA-2.0', 'cc-by-sa-2.0',
+ 'Creative Commons Attribution - ShareAlike 2.0',
+ 'http://creativecommons.org/licenses/by-sa/2.0/' );
+ assert.strictEqual( license.isCc(), true, 'CC license recognized' );
+
+ license = new mw.mmv.model.License( 'Public Domain', 'pd',
+ 'Public Domain for lack of originality' );
+ assert.strictEqual( license.isCc(), false, 'Non-CC license not recognized' );
+
+ license = new mw.mmv.model.License( 'MIT' );
+ assert.strictEqual( license.isCc(), false, 'Non-CC license with no internal name not recognized' );
+ } );
+
+ QUnit.test( 'isPd()', function ( assert ) {
+ var license;
+
+ license = new mw.mmv.model.License( 'Public Domain', 'pd',
+ 'Public Domain for lack of originality' );
+ assert.strictEqual( license.isPd(), true, 'PD license recognized' );
+
+ license = new mw.mmv.model.License( 'CC-BY-SA-2.0', 'cc-by-sa-2.0',
+ 'Creative Commons Attribution - ShareAlike 2.0',
+ 'http://creativecommons.org/licenses/by-sa/2.0/' );
+ assert.strictEqual( license.isPd(), false, 'Non-PD license not recognized' );
+
+ license = new mw.mmv.model.License( 'MIT' );
+ assert.strictEqual( license.isPd(), false, 'Non-PD license with no internal name not recognized' );
+ } );
+
+ QUnit.test( 'isFree()', function ( assert ) {
+ var license;
+
+ license = new mw.mmv.model.License( 'CC-BY-SA-2.0', 'cc-by-sa-2.0',
+ 'Creative Commons Attribution - ShareAlike 2.0',
+ 'http://creativecommons.org/licenses/by-sa/2.0/' );
+ assert.strictEqual( license.isFree(), true, 'Licenses default to free' );
+
+ license = new mw.mmv.model.License( 'Fair use', 'fairuse',
+ 'Fair use', undefined, undefined, true );
+ assert.strictEqual( license.isFree(), false, 'Non-free flag handled correctly' );
+ } );
+
+ QUnit.test( 'needsAttribution()', function ( assert ) {
+ var license;
+
+ license = new mw.mmv.model.License( 'CC-BY-SA-2.0', 'cc-by-sa-2.0',
+ 'Creative Commons Attribution - ShareAlike 2.0',
+ 'http://creativecommons.org/licenses/by-sa/2.0/' );
+ assert.strictEqual( license.needsAttribution(), true, 'Licenses assumed to need attribution by default' );
+
+ license = new mw.mmv.model.License( 'Public Domain', 'pd',
+ 'Public Domain for lack of originality', false );
+ assert.strictEqual( license.needsAttribution(), false, 'Attribution required flag handled correctly' );
+ } );
+}( mediaWiki, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.Repo.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.Repo.test.js
new file mode 100644
index 00000000..a9bc7a2d
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.Repo.test.js
@@ -0,0 +1,100 @@
+/*
+ * This file is part of the MediaWiki extension MediaViewer.
+ *
+ * MediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw ) {
+ QUnit.module( 'mmv.model.Repo', QUnit.newMwEnvironment() );
+
+ QUnit.test( 'Repo constructor sanity check', function ( assert ) {
+ var displayName = 'Wikimedia Commons',
+ favicon = '//commons.wikimedia.org/favicon.ico',
+ apiUrl = '//commons.wikimedia.org/w/api.php',
+ server = '//commons.wikimedia.org',
+ articlePath = '//commons.wikimedia.org/wiki/$1',
+ descBaseUrl = '//commons.wikimedia.org/wiki/File:',
+ localRepo = new mw.mmv.model.Repo( displayName, favicon, true ),
+ foreignApiRepo = new mw.mmv.model.ForeignApiRepo( displayName, favicon,
+ false, apiUrl, server, articlePath ),
+ foreignDbRepo = new mw.mmv.model.ForeignDbRepo( displayName, favicon, false, descBaseUrl );
+
+ assert.ok( localRepo, 'Local repo creation works' );
+ assert.ok( foreignApiRepo,
+ 'Foreign API repo creation works' );
+ assert.ok( foreignDbRepo, 'Foreign DB repo creation works' );
+ } );
+
+ QUnit.test( 'getArticlePath()', function ( assert ) {
+ var displayName = 'Wikimedia Commons',
+ favicon = '//commons.wikimedia.org/favicon.ico',
+ apiUrl = '//commons.wikimedia.org/w/api.php',
+ server = '//commons.wikimedia.org',
+ articlePath = '/wiki/$1',
+ descBaseUrl = '//commons.wikimedia.org/wiki/File:',
+ localRepo = new mw.mmv.model.Repo( displayName, favicon, true ),
+ foreignApiRepo = new mw.mmv.model.ForeignApiRepo( displayName, favicon,
+ false, apiUrl, server, articlePath ),
+ foreignDbRepo = new mw.mmv.model.ForeignDbRepo( displayName, favicon, false, descBaseUrl ),
+ expectedLocalArticlePath = '/wiki/$1',
+ expectedFullArticlePath = '//commons.wikimedia.org/wiki/$1',
+ oldWgArticlePath = mw.config.get( 'wgArticlePath' ),
+ oldWgServer = mw.config.get( 'wgServer' );
+
+ mw.config.set( 'wgArticlePath', '/wiki/$1' );
+ mw.config.set( 'wgServer', server );
+
+ assert.strictEqual( localRepo.getArticlePath(), expectedLocalArticlePath,
+ 'Local repo article path is correct' );
+ assert.strictEqual( localRepo.getArticlePath( true ), expectedFullArticlePath,
+ 'Local repo absolute article path is correct' );
+ assert.strictEqual( foreignApiRepo.getArticlePath(), expectedFullArticlePath,
+ 'Foreign API article path is correct' );
+ assert.strictEqual( foreignDbRepo.getArticlePath(), expectedFullArticlePath,
+ 'Foreign DB article path is correct' );
+
+ mw.config.set( 'wgArticlePath', oldWgArticlePath );
+ mw.config.set( 'wgServer', oldWgServer );
+ } );
+
+ QUnit.test( 'getSiteLink()', function ( assert ) {
+ var displayName = 'Wikimedia Commons',
+ favicon = '//commons.wikimedia.org/favicon.ico',
+ apiUrl = '//commons.wikimedia.org/w/api.php',
+ server = '//commons.wikimedia.org',
+ articlePath = '/wiki/$1',
+ descBaseUrl = '//commons.wikimedia.org/wiki/File:',
+ localRepo = new mw.mmv.model.Repo( displayName, favicon, true ),
+ foreignApiRepo = new mw.mmv.model.ForeignApiRepo( displayName, favicon,
+ false, apiUrl, server, articlePath ),
+ foreignDbRepo = new mw.mmv.model.ForeignDbRepo( displayName, favicon, false, descBaseUrl ),
+ expectedSiteLink = '//commons.wikimedia.org/wiki/',
+ oldWgArticlePath = mw.config.get( 'wgArticlePath' ),
+ oldWgServer = mw.config.get( 'wgServer' );
+
+ mw.config.set( 'wgArticlePath', '/wiki/$1' );
+ mw.config.set( 'wgServer', server );
+
+ assert.strictEqual( localRepo.getSiteLink(), expectedSiteLink,
+ 'Local repo site link is correct' );
+ assert.strictEqual( foreignApiRepo.getSiteLink(), expectedSiteLink,
+ 'Foreign API repo site link is correct' );
+ assert.strictEqual( foreignDbRepo.getSiteLink(), expectedSiteLink,
+ 'Foreign DB repo site link is correct' );
+
+ mw.config.set( 'wgArticlePath', oldWgArticlePath );
+ mw.config.set( 'wgServer', oldWgServer );
+ } );
+
+}( mediaWiki ) );
diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.TaskQueue.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.TaskQueue.test.js
new file mode 100644
index 00000000..f3958a24
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.TaskQueue.test.js
@@ -0,0 +1,276 @@
+/*
+ * This file is part of the MediaWiki extension MediaViewer.
+ *
+ * MediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $ ) {
+ QUnit.module( 'mmv.model.TaskQueue', QUnit.newMwEnvironment() );
+
+ QUnit.test( 'TaskQueue constructor sanity check', function ( assert ) {
+ var taskQueue = new mw.mmv.model.TaskQueue();
+
+ assert.ok( taskQueue, 'TaskQueue created successfully' );
+ } );
+
+ QUnit.test( 'Queue length check', function ( assert ) {
+ var taskQueue = new mw.mmv.model.TaskQueue();
+
+ assert.strictEqual( taskQueue.queue.length, 0, 'queue is initially empty' );
+
+ taskQueue.push( function () {} );
+
+ assert.strictEqual( taskQueue.queue.length, 1, 'queue length is incremented on push' );
+ } );
+
+ QUnit.test( 'State check', function ( assert ) {
+ var taskQueue = new mw.mmv.model.TaskQueue(),
+ task = $.Deferred(),
+ promise;
+
+ taskQueue.push( function () { return task; } );
+
+ assert.strictEqual( taskQueue.state, mw.mmv.model.TaskQueue.State.NOT_STARTED,
+ 'state is initially NOT_STARTED' );
+
+ promise = taskQueue.execute().then( function () {
+ assert.strictEqual( taskQueue.state, mw.mmv.model.TaskQueue.State.FINISHED,
+ 'state is FINISHED after execution finished' );
+ } );
+
+ assert.strictEqual( taskQueue.state, mw.mmv.model.TaskQueue.State.RUNNING,
+ 'state is RUNNING after execution started' );
+
+ task.resolve();
+
+ return promise;
+ } );
+
+ QUnit.test( 'State check for cancellation', function ( assert ) {
+ var taskQueue = new mw.mmv.model.TaskQueue(),
+ task = $.Deferred();
+
+ taskQueue.push( function () { return task; } );
+ taskQueue.execute();
+ taskQueue.cancel();
+
+ assert.strictEqual( taskQueue.state, mw.mmv.model.TaskQueue.State.CANCELLED,
+ 'state is CANCELLED after cancellation' );
+ } );
+
+ QUnit.test( 'Test executing empty queue', function ( assert ) {
+ var taskQueue = new mw.mmv.model.TaskQueue();
+
+ return taskQueue.execute().done( function () {
+ assert.ok( true, 'Queue promise resolved' );
+ } );
+ } );
+
+ QUnit.test( 'Simple execution test', function ( assert ) {
+ var taskQueue = new mw.mmv.model.TaskQueue(),
+ called = false;
+
+ taskQueue.push( function () {
+ called = true;
+ } );
+
+ return taskQueue.execute().then( function () {
+ assert.strictEqual( called, true, 'Task executed successfully' );
+ } );
+ } );
+
+ QUnit.test( 'Task execution order test', function ( assert ) {
+ var taskQueue = new mw.mmv.model.TaskQueue(),
+ order = [];
+
+ taskQueue.push( function () {
+ order.push( 1 );
+ } );
+
+ taskQueue.push( function () {
+ var deferred = $.Deferred();
+
+ order.push( 2 );
+
+ setTimeout( function () {
+ deferred.resolve();
+ }, 0 );
+
+ return deferred;
+ } );
+
+ taskQueue.push( function () {
+ order.push( 3 );
+ } );
+
+ return taskQueue.execute().then( function () {
+ assert.deepEqual( order, [ 1, 2, 3 ], 'Tasks executed in order' );
+ } );
+ } );
+
+ QUnit.test( 'Double execution test', function ( assert ) {
+ var taskQueue = new mw.mmv.model.TaskQueue(),
+ called = 0;
+
+ taskQueue.push( function () {
+ called++;
+ } );
+
+ return taskQueue.execute().then( function () {
+ return taskQueue.execute();
+ } ).then( function () {
+ assert.strictEqual( called, 1, 'Task executed only once' );
+ } );
+ } );
+
+ QUnit.test( 'Parallel execution test', function ( assert ) {
+ var taskQueue = new mw.mmv.model.TaskQueue(),
+ called = 0;
+
+ taskQueue.push( function () {
+ called++;
+ } );
+
+ return $.when(
+ taskQueue.execute(),
+ taskQueue.execute()
+ ).then( function () {
+ assert.strictEqual( called, 1, 'Task executed only once' );
+ } );
+ } );
+
+ QUnit.test( 'Test push after execute', function ( assert ) {
+ var taskQueue = new mw.mmv.model.TaskQueue();
+
+ taskQueue.execute();
+
+ try {
+ taskQueue.push( function () {} );
+ } catch ( e ) {
+ assert.ok( e, 'Exception thrown when trying to push to an already running queue' );
+ }
+ } );
+
+ QUnit.test( 'Test failed task', function ( assert ) {
+ var taskQueue = new mw.mmv.model.TaskQueue();
+
+ taskQueue.push( function () {
+ return $.Deferred().reject();
+ } );
+
+ return taskQueue.execute().done( function () {
+ assert.ok( true, 'Queue promise resolved' );
+ } );
+ } );
+
+ QUnit.test( 'Test that tasks wait for each other', function ( assert ) {
+ var taskQueue = new mw.mmv.model.TaskQueue(),
+ longRunningTaskFinished = false,
+ seenFinished = false;
+
+ taskQueue.push( function () {
+ var deferred = $.Deferred();
+
+ setTimeout( function () {
+ longRunningTaskFinished = true;
+ deferred.resolve();
+ }, 0 );
+
+ return deferred;
+ } );
+
+ taskQueue.push( function () {
+ seenFinished = longRunningTaskFinished;
+ } );
+
+ return taskQueue.execute().then( function () {
+ assert.ok( seenFinished, 'Task waits for previous task to finish' );
+ } );
+ } );
+
+ QUnit.test( 'Test cancellation before start', function ( assert ) {
+ var taskQueue = new mw.mmv.model.TaskQueue(),
+ triggered = false,
+ verificationTask = function () {
+ triggered = true;
+ };
+
+ taskQueue.push( verificationTask );
+
+ taskQueue.cancel();
+
+ taskQueue.execute()
+ .done( function () {
+ assert.ok( false, 'Queue promise rejected' );
+ } )
+ .fail( function () {
+ assert.ok( true, 'Queue promise rejected' );
+ assert.strictEqual( triggered, false, 'Task was not triggered' );
+ } )
+ .always( assert.async() );
+ } );
+
+ QUnit.test( 'Test cancellation within callback', function ( assert ) {
+ var taskQueue = new mw.mmv.model.TaskQueue(),
+ triggered = false,
+ verificationTask = function () {
+ triggered = true;
+ };
+
+ taskQueue.push( function () {
+ taskQueue.cancel();
+ } );
+ taskQueue.push( verificationTask );
+
+ taskQueue.execute()
+ .done( function () {
+ assert.ok( false, 'Queue promise rejected' );
+ } )
+ .fail( function () {
+ assert.ok( true, 'Queue promise rejected' );
+ assert.strictEqual( triggered, false, 'Task was not triggered' );
+ } )
+ .always( assert.async() );
+ } );
+
+ QUnit.test( 'Test cancellation from task', function ( assert ) {
+ var taskQueue = new mw.mmv.model.TaskQueue(),
+ triggered = false,
+ task1 = $.Deferred(),
+ verificationTask = function () {
+ triggered = true;
+ };
+
+ taskQueue.push( function () {
+ return task1;
+ } );
+ taskQueue.push( verificationTask );
+
+ setTimeout( function () {
+ taskQueue.cancel();
+ task1.resolve();
+ }, 0 );
+
+ taskQueue.execute()
+ .done( function () {
+ assert.ok( false, 'Queue promise rejected' );
+ } )
+ .fail( function () {
+ assert.ok( true, 'Queue promise rejected' );
+ assert.strictEqual( triggered, false, 'Task was not triggered' );
+ } )
+ .always( assert.async() );
+ } );
+
+}( mediaWiki, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.test.js
new file mode 100644
index 00000000..a24241dc
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/model/mmv.model.test.js
@@ -0,0 +1,58 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw ) {
+ QUnit.module( 'mmv.model', QUnit.newMwEnvironment() );
+
+ QUnit.test( 'Thumbnail constructor sanity check', function ( assert ) {
+ var width = 23,
+ height = 42,
+ url = 'http://example.com/foo.jpg',
+ thumbnail = new mw.mmv.model.Thumbnail( url, width, height );
+
+ assert.strictEqual( thumbnail.url, url, 'Url is set correctly' );
+ assert.strictEqual( thumbnail.width, width, 'Width is set correctly' );
+ assert.strictEqual( thumbnail.height, height, 'Height is set correctly' );
+
+ try {
+ thumbnail = new mw.mmv.model.Thumbnail( url, width );
+ } catch ( e ) {
+ assert.ok( e, 'Exception is thrown when parameters are missing' );
+ }
+ } );
+
+ QUnit.test( 'ThumbnailWidth constructor sanity check', function ( assert ) {
+ var cssWidth = 23,
+ cssHeight = 29,
+ screenWidth = 42,
+ realWidth = 123,
+ thumbnailWidth = new mw.mmv.model.ThumbnailWidth(
+ cssWidth, cssHeight, screenWidth, realWidth );
+
+ assert.strictEqual( thumbnailWidth.cssWidth, cssWidth, 'Width is set correctly' );
+ assert.strictEqual( thumbnailWidth.cssHeight, cssHeight, 'Height is set correctly' );
+ assert.strictEqual( thumbnailWidth.screen, screenWidth, 'Screen width is set correctly' );
+ assert.strictEqual( thumbnailWidth.real, realWidth, 'Real width is set correctly' );
+
+ try {
+ thumbnailWidth = new mw.mmv.model.ThumbnailWidth( cssWidth, screenWidth );
+ } catch ( e ) {
+ assert.ok( e, 'Exception is thrown when parameters are missing' );
+ }
+ } );
+
+}( mediaWiki ) );
diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/provider/mmv.provider.Api.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/provider/mmv.provider.Api.test.js
new file mode 100644
index 00000000..371a22a5
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/provider/mmv.provider.Api.test.js
@@ -0,0 +1,270 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $ ) {
+ QUnit.module( 'mmv.provider.Api', QUnit.newMwEnvironment() );
+
+ QUnit.test( 'Api constructor sanity check', function ( assert ) {
+ var api = { get: function () {} },
+ options = {},
+ apiProvider = new mw.mmv.provider.Api( api, options ),
+ ApiProviderWithNoOptions = new mw.mmv.provider.Api( api );
+
+ assert.ok( apiProvider );
+ assert.ok( ApiProviderWithNoOptions );
+ } );
+
+ QUnit.test( 'apiGetWithMaxAge()', function ( assert ) {
+ var api = {},
+ options = {},
+ apiProvider = new mw.mmv.provider.Api( api, options );
+
+ api.get = this.sandbox.stub();
+ apiProvider.apiGetWithMaxAge( {} );
+ assert.ok( !( 'maxage' in api.get.getCall( 0 ).args[ 0 ] ), 'maxage is not set by default' );
+ assert.ok( !( 'smaxage' in api.get.getCall( 0 ).args[ 0 ] ), 'smaxage is not set by default' );
+
+ options = { maxage: 123 };
+ apiProvider = new mw.mmv.provider.Api( api, options );
+
+ api.get = this.sandbox.stub();
+ apiProvider.apiGetWithMaxAge( {} );
+ assert.strictEqual( api.get.getCall( 0 ).args[ 0 ].maxage, 123, 'maxage falls back to provider default' );
+ assert.strictEqual( api.get.getCall( 0 ).args[ 0 ].smaxage, 123, 'smaxage falls back to provider default' );
+
+ api.get = this.sandbox.stub();
+ apiProvider.apiGetWithMaxAge( {}, null, 456 );
+ assert.strictEqual( api.get.getCall( 0 ).args[ 0 ].maxage, 456, 'maxage can be overridden' );
+ assert.strictEqual( api.get.getCall( 0 ).args[ 0 ].smaxage, 456, 'smaxage can be overridden' );
+
+ api.get = this.sandbox.stub();
+ apiProvider.apiGetWithMaxAge( {}, null, null );
+ assert.ok( !( 'maxage' in api.get.getCall( 0 ).args[ 0 ] ), 'maxage can be overridden to unset' );
+ assert.ok( !( 'smaxage' in api.get.getCall( 0 ).args[ 0 ] ), 'smaxage can be overridden to unset' );
+ } );
+
+ QUnit.test( 'getCachedPromise success', function ( assert ) {
+ var api = { get: function () {} },
+ apiProvider = new mw.mmv.provider.Api( api ),
+ oldMwLog = mw.log,
+ promiseSource,
+ promiseShouldBeCached = false;
+
+ mw.log = function () {
+ assert.ok( false, 'mw.log should not have been called' );
+ };
+
+ promiseSource = function ( result ) {
+ return function () {
+ assert.ok( !promiseShouldBeCached, 'promise was not cached' );
+ return $.Deferred().resolve( result );
+ };
+ };
+
+ apiProvider.getCachedPromise( 'foo', promiseSource( 1 ) ).done( function ( result ) {
+ assert.strictEqual( result, 1, 'result comes from the promise source' );
+ } );
+
+ apiProvider.getCachedPromise( 'bar', promiseSource( 2 ) ).done( function ( result ) {
+ assert.strictEqual( result, 2, 'result comes from the promise source' );
+ } );
+
+ promiseShouldBeCached = true;
+ apiProvider.getCachedPromise( 'foo', promiseSource( 3 ) ).done( function ( result ) {
+ assert.strictEqual( result, 1, 'result comes from cache' );
+ } );
+
+ mw.log = oldMwLog;
+ } );
+
+ QUnit.test( 'getCachedPromise failure', function ( assert ) {
+ var api = { get: function () {} },
+ apiProvider = new mw.mmv.provider.Api( api ),
+ oldMwLog = mw.log,
+ promiseSource,
+ promiseShouldBeCached = false;
+
+ mw.log = function () {
+ assert.ok( true, 'mw.log was called' );
+ };
+
+ promiseSource = function ( result ) {
+ return function () {
+ assert.ok( !promiseShouldBeCached, 'promise was not cached' );
+ return $.Deferred().reject( result );
+ };
+ };
+
+ apiProvider.getCachedPromise( 'foo', promiseSource( 1 ) ).fail( function ( result ) {
+ assert.strictEqual( result, 1, 'result comes from the promise source' );
+ } );
+
+ apiProvider.getCachedPromise( 'bar', promiseSource( 2 ) ).fail( function ( result ) {
+ assert.strictEqual( result, 2, 'result comes from the promise source' );
+ } );
+
+ promiseShouldBeCached = true;
+ apiProvider.getCachedPromise( 'foo', promiseSource( 3 ) ).fail( function ( result ) {
+ assert.strictEqual( result, 1, 'result comes from cache' );
+ } );
+
+ mw.log = oldMwLog;
+ } );
+
+ QUnit.test( 'getErrorMessage', function ( assert ) {
+ var api = { get: function () {} },
+ apiProvider = new mw.mmv.provider.Api( api ),
+ errorMessage;
+
+ errorMessage = apiProvider.getErrorMessage( {
+ servedby: 'mw1194',
+ error: {
+ code: 'unknown_action',
+ info: 'Unrecognized value for parameter \'action\': FOO'
+ }
+ } );
+ assert.strictEqual( errorMessage,
+ 'unknown_action: Unrecognized value for parameter \'action\': FOO',
+ 'error message is parsed correctly' );
+
+ assert.strictEqual( apiProvider.getErrorMessage( {} ), 'unknown error', 'missing error message is handled' );
+ } );
+
+ QUnit.test( 'getNormalizedTitle', function ( assert ) {
+ var api = { get: function () {} },
+ apiProvider = new mw.mmv.provider.Api( api ),
+ title = new mw.Title( 'Image:Stuff.jpg' ),
+ normalizedTitle;
+
+ normalizedTitle = apiProvider.getNormalizedTitle( title, {} );
+ assert.strictEqual( normalizedTitle, title, 'missing normalization block is handled' );
+
+ normalizedTitle = apiProvider.getNormalizedTitle( title, {
+ query: {
+ normalized: [
+ {
+ from: 'Image:Foo.jpg',
+ to: 'File:Foo.jpg'
+ }
+ ]
+ }
+ } );
+ assert.strictEqual( normalizedTitle, title, 'irrelevant normalization info is skipped' );
+
+ normalizedTitle = apiProvider.getNormalizedTitle( title, {
+ query: {
+ normalized: [
+ {
+ from: 'Image:Stuff.jpg',
+ to: 'File:Stuff.jpg'
+ }
+ ]
+ }
+ } );
+ assert.strictEqual( normalizedTitle.getPrefixedDb(), 'File:Stuff.jpg', 'normalization happens' );
+ } );
+
+ QUnit.test( 'getQueryField', function ( assert ) {
+ var api = { get: function () {} },
+ apiProvider = new mw.mmv.provider.Api( api ),
+ done = assert.async( 3 ),
+ data;
+
+ data = {
+ query: {
+ imageusage: [
+ {
+ pageid: 736,
+ ns: 0,
+ title: 'Albert Einstein'
+ }
+ ]
+ }
+ };
+
+ apiProvider.getQueryField( 'imageusage', data ).then( function ( field ) {
+ assert.strictEqual( field, data.query.imageusage, 'specified field is found' );
+ done();
+ } );
+ apiProvider.getQueryField( 'imageusage', {} ).fail( function () {
+ assert.ok( true, 'promise rejected when data is missing' );
+ done();
+ } );
+
+ apiProvider.getQueryField( 'imageusage', { data: { query: {} } } ).fail( function () {
+ assert.ok( true, 'promise rejected when field is missing' );
+ done();
+ } );
+ } );
+
+ QUnit.test( 'getQueryPage', function ( assert ) {
+ var api = { get: function () {} },
+ apiProvider = new mw.mmv.provider.Api( api ),
+ title = new mw.Title( 'File:Stuff.jpg' ),
+ titleWithNamespaceAlias = new mw.Title( 'Image:Stuff.jpg' ),
+ otherTitle = new mw.Title( 'File:Foo.jpg' ),
+ done = assert.async( 6 ),
+ data;
+
+ data = {
+ normalized: [
+ {
+ from: 'Image:Stuff.jpg',
+ to: 'File:Stuff.jpg'
+ }
+ ],
+ query: {
+ pages: {
+ '-1': {
+ title: 'File:Stuff.jpg'
+ }
+ }
+ }
+ };
+
+ apiProvider.getQueryPage( title, data ).then( function ( field ) {
+ assert.strictEqual( field, data.query.pages[ '-1' ], 'specified page is found' );
+ done();
+ } );
+
+ apiProvider.getQueryPage( titleWithNamespaceAlias, data ).then( function ( field ) {
+ assert.strictEqual( field, data.query.pages[ '-1' ],
+ 'specified page is found even if its title was normalized' );
+ done();
+ } );
+
+ apiProvider.getQueryPage( otherTitle, {} ).fail( function () {
+ assert.ok( true, 'promise rejected when page has different title' );
+ done();
+ } );
+
+ apiProvider.getQueryPage( title, {} ).fail( function () {
+ assert.ok( true, 'promise rejected when data is missing' );
+ done();
+ } );
+
+ apiProvider.getQueryPage( title, { data: { query: {} } } ).fail( function () {
+ assert.ok( true, 'promise rejected when pages are missing' );
+ done();
+ } );
+
+ apiProvider.getQueryPage( title, { data: { query: { pages: {} } } } ).fail( function () {
+ assert.ok( true, 'promise rejected when pages are empty' );
+ done();
+ } );
+ } );
+}( mediaWiki, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/provider/mmv.provider.FileRepoInfo.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/provider/mmv.provider.FileRepoInfo.test.js
new file mode 100644
index 00000000..80771784
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/provider/mmv.provider.FileRepoInfo.test.js
@@ -0,0 +1,126 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $ ) {
+ QUnit.module( 'mmv.provider.FileRepoInfo', QUnit.newMwEnvironment() );
+
+ QUnit.test( 'FileRepoInfo constructor sanity check', function ( assert ) {
+ var api = { get: function () {} },
+ fileRepoInfoProvider = new mw.mmv.provider.FileRepoInfo( api );
+
+ assert.ok( fileRepoInfoProvider );
+ } );
+
+ QUnit.test( 'FileRepoInfo get test', function ( assert ) {
+ var apiCallCount = 0,
+ api = { get: function () {
+ apiCallCount++;
+ return $.Deferred().resolve( {
+ query: {
+ repos: [
+ {
+ name: 'shared',
+ displayname: 'Wikimedia Commons',
+ rootUrl: '//upload.beta.wmflabs.org/wikipedia/commons',
+ local: false,
+ url: '//upload.beta.wmflabs.org/wikipedia/commons',
+ thumbUrl: '//upload.beta.wmflabs.org/wikipedia/commons/thumb',
+ initialCapital: true,
+ descBaseUrl: '//commons.wikimedia.beta.wmflabs.org/wiki/File:',
+ scriptDirUrl: '//commons.wikimedia.beta.wmflabs.org/w',
+ fetchDescription: true,
+ favicon: 'http://en.wikipedia.org/favicon.ico'
+ },
+ {
+ name: 'wikimediacommons',
+ displayname: 'Wikimedia Commons',
+ rootUrl: '//upload.beta.wmflabs.org/wikipedia/en',
+ local: false,
+ url: '//upload.beta.wmflabs.org/wikipedia/en',
+ thumbUrl: '//upload.beta.wmflabs.org/wikipedia/en/thumb',
+ initialCapital: true,
+ scriptDirUrl: 'http://commons.wikimedia.org/w',
+ fetchDescription: true,
+ descriptionCacheExpiry: 43200,
+ apiurl: 'http://commons.wikimedia.org/w/api.php',
+ articlepath: '/wiki/$1',
+ server: '//commons.wikimedia.org',
+ favicon: '//commons.wikimedia.org/favicon.ico'
+ },
+ {
+ name: 'local',
+ displayname: null,
+ rootUrl: '//upload.beta.wmflabs.org/wikipedia/en',
+ local: true,
+ url: '//upload.beta.wmflabs.org/wikipedia/en',
+ thumbUrl: '//upload.beta.wmflabs.org/wikipedia/en/thumb',
+ initialCapital: true,
+ scriptDirUrl: '/w',
+ favicon: 'http://en.wikipedia.org/favicon.ico'
+ }
+ ]
+ }
+ } );
+ } },
+ fileRepoInfoProvider = new mw.mmv.provider.FileRepoInfo( api );
+
+ return fileRepoInfoProvider.get().then( function ( repos ) {
+ assert.strictEqual( repos.shared.displayName,
+ 'Wikimedia Commons', 'displayName is set correctly' );
+ assert.strictEqual( repos.shared.favIcon,
+ 'http://en.wikipedia.org/favicon.ico', 'favIcon is set correctly' );
+ assert.strictEqual( repos.shared.isLocal, false, 'isLocal is set correctly' );
+ assert.strictEqual( repos.shared.descBaseUrl,
+ '//commons.wikimedia.beta.wmflabs.org/wiki/File:', 'descBaseUrl is set correctly' );
+
+ assert.strictEqual( repos.wikimediacommons.displayName,
+ 'Wikimedia Commons', 'displayName is set correctly' );
+ assert.strictEqual( repos.wikimediacommons.favIcon,
+ '//commons.wikimedia.org/favicon.ico', 'favIcon is set correctly' );
+ assert.strictEqual( repos.wikimediacommons.isLocal, false, 'isLocal is set correctly' );
+ assert.strictEqual( repos.wikimediacommons.apiUrl,
+ 'http://commons.wikimedia.org/w/api.php', 'apiUrl is set correctly' );
+ assert.strictEqual( repos.wikimediacommons.server,
+ '//commons.wikimedia.org', 'server is set correctly' );
+ assert.strictEqual( repos.wikimediacommons.articlePath,
+ '/wiki/$1', 'articlePath is set correctly' );
+
+ assert.strictEqual( repos.local.displayName, null, 'displayName is set correctly' );
+ assert.strictEqual( repos.local.favIcon,
+ 'http://en.wikipedia.org/favicon.ico', 'favIcon is set correctly' );
+ assert.strictEqual( repos.local.isLocal, true, 'isLocal is set correctly' );
+ } ).then( function () {
+ // call the data provider a second time to check caching
+ return fileRepoInfoProvider.get();
+ } ).then( function () {
+ assert.strictEqual( apiCallCount, 1 );
+ } );
+ } );
+
+ QUnit.test( 'FileRepoInfo fail test', function ( assert ) {
+ var api = { get: function () {
+ return $.Deferred().resolve( {} );
+ } },
+ done = assert.async(),
+ fileRepoInfoProvider = new mw.mmv.provider.FileRepoInfo( api );
+
+ fileRepoInfoProvider.get().fail( function () {
+ assert.ok( true, 'promise rejected when no data is returned' );
+ done();
+ } );
+ } );
+}( mediaWiki, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/provider/mmv.provider.GuessedThumbnailInfo.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/provider/mmv.provider.GuessedThumbnailInfo.test.js
new file mode 100644
index 00000000..93cd51fa
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/provider/mmv.provider.GuessedThumbnailInfo.test.js
@@ -0,0 +1,280 @@
+/*
+ * This file is part of the MediaWiki extension MediaViewer.
+ *
+ * MediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw ) {
+ QUnit.module( 'mmv.provider.GuessedThumbnailInfo', QUnit.newMwEnvironment() );
+
+ QUnit.test( 'Constructor sanity check', function ( assert ) {
+ var provider = new mw.mmv.provider.GuessedThumbnailInfo();
+ assert.ok( provider, 'Constructor call successful' );
+ } );
+
+ QUnit.test( 'get()', function ( assert ) {
+ var provider = new mw.mmv.provider.GuessedThumbnailInfo(),
+ file = new mw.Title( 'File:Copyleft.svg' ),
+ sampleUrl = 'http://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/Copyleft.svg/180px-Copyleft.svg.png',
+ width = 300,
+ originalWidth = 512,
+ originalHeight = 512,
+ resultUrl = 'http://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/Copyleft.svg/300px-Copyleft.svg.png',
+ done = assert.async(),
+ result;
+
+ provider.getUrl = function () { return resultUrl; };
+ result = provider.get( file, sampleUrl, width, originalWidth, originalHeight );
+ assert.ok( result.then, 'Result is a promise' );
+ assert.strictEqual( result.state(), 'resolved', 'Result is resolved' );
+ result.then( function ( thumbnailInfo ) {
+ assert.ok( thumbnailInfo.width, 'Width is set' );
+ assert.ok( thumbnailInfo.height, 'Height is set' );
+ assert.strictEqual( thumbnailInfo.url, resultUrl, 'URL is set' );
+ done();
+ } );
+
+ provider.getUrl = function () { return undefined; };
+ result = provider.get( file, sampleUrl, width, originalWidth, originalHeight );
+ assert.ok( result.then, 'Result is a promise' );
+ assert.strictEqual( result.state(), 'rejected', 'Result is rejected' );
+ } );
+
+ QUnit.test( 'getUrl()', function ( assert ) {
+ var provider = new mw.mmv.provider.GuessedThumbnailInfo(),
+ file = new mw.Title( 'File:Elizabeth_I_George_Gower.jpg' ),
+ originalWidth = 922,
+ originalHeight = 968,
+ width,
+ sampleUrl,
+ expectedUrl,
+ resultUrl;
+
+ sampleUrl = 'http://upload.wikimedia.org/wikipedia/commons/7/78/Elizabeth_I_George_Gower.jpg';
+ width = 1000;
+ expectedUrl = 'http://upload.wikimedia.org/wikipedia/commons/7/78/Elizabeth_I_George_Gower.jpg';
+ resultUrl = provider.getUrl( file, sampleUrl, width, originalWidth, originalHeight );
+ assert.strictEqual( resultUrl, expectedUrl, 'Simple case - full image, needs no resize' );
+
+ sampleUrl = 'http://upload.wikimedia.org/wikipedia/commons/thumb/7/78/Elizabeth_I_George_Gower.jpg/180px-Elizabeth_I_George_Gower.jpg';
+ width = 400;
+ expectedUrl = 'http://upload.wikimedia.org/wikipedia/commons/thumb/7/78/Elizabeth_I_George_Gower.jpg/400px-Elizabeth_I_George_Gower.jpg';
+ resultUrl = provider.getUrl( file, sampleUrl, width, originalWidth, originalHeight );
+ assert.strictEqual( resultUrl, expectedUrl, 'Mostly simple case - just need to replace size' );
+
+ sampleUrl = 'http://upload.wikimedia.org/wikipedia/commons/7/78/Elizabeth_I_George_Gower.jpg';
+ width = 400;
+ expectedUrl = undefined;
+ resultUrl = provider.getUrl( file, sampleUrl, width, originalWidth, originalHeight );
+ assert.strictEqual( resultUrl, expectedUrl, 'We bail on hard case - full to thumbnail' );
+
+ sampleUrl = 'http://upload.wikimedia.org/wikipedia/commons/thumb/7/78/Elizabeth_I_George_Gower.jpg/180px-Elizabeth_I_George_Gower.jpg';
+ width = 1000;
+ expectedUrl = 'http://upload.wikimedia.org/wikipedia/commons/7/78/Elizabeth_I_George_Gower.jpg';
+ resultUrl = provider.getUrl( file, sampleUrl, width, originalWidth, originalHeight );
+ assert.strictEqual( resultUrl, expectedUrl, 'Thumbnail to full-size, image with limited size' );
+
+ file = new mw.Title( 'File:Ranunculus_gmelinii_NRCS-2.tiff' );
+ sampleUrl = 'https://upload.wikimedia.org/wikipedia/commons/thumb/2/27/Ranunculus_gmelinii_NRCS-2.tiff/lossy-page1-428px-Ranunculus_gmelinii_NRCS-2.tiff.jpg';
+ width = 2000;
+ originalWidth = 1500;
+ originalHeight = 2100;
+ expectedUrl = 'https://upload.wikimedia.org/wikipedia/commons/thumb/2/27/Ranunculus_gmelinii_NRCS-2.tiff/lossy-page1-1500px-Ranunculus_gmelinii_NRCS-2.tiff.jpg';
+ resultUrl = provider.getUrl( file, sampleUrl, width, originalWidth, originalHeight );
+ assert.strictEqual( resultUrl, expectedUrl, 'Thumbnail to full-size, image which cannot be displayed directly' );
+
+ file = new mw.Title( 'File:Copyleft.svg' );
+ sampleUrl = 'http://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/Copyleft.svg/180px-Copyleft.svg.png';
+ width = 1000;
+ originalWidth = 512;
+ originalHeight = 512;
+ expectedUrl = 'http://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/Copyleft.svg/1000px-Copyleft.svg.png';
+ resultUrl = provider.getUrl( file, sampleUrl, width, originalWidth, originalHeight );
+ assert.strictEqual( resultUrl, expectedUrl, 'Thumbnail to "full-size", image with unlimited size' );
+ } );
+
+ QUnit.test( 'needsOriginal()', function ( assert ) {
+ var provider = new mw.mmv.provider.GuessedThumbnailInfo(),
+ file = new mw.Title( 'File:Copyleft.svg' );
+
+ assert.ok( !provider.needsOriginal( file, 100, 1000 ), 'Thumbnail of an SVG smaller than the original size doesn\'t need original' );
+ assert.ok( !provider.needsOriginal( file, 1000, 1000 ), 'Thumbnail of an SVG equal to the original size doesn\'t need original' );
+ assert.ok( !provider.needsOriginal( file, 2000, 1000 ), 'Thumbnail of an SVG bigger than the original size doesn\'t need original' );
+
+ file = new mw.Title( 'File:Foo.png' );
+
+ assert.ok( !provider.needsOriginal( file, 100, 1000 ), 'Thumbnail of a PNG smaller than the original size doesn\'t need original' );
+ assert.ok( provider.needsOriginal( file, 1000, 1000 ), 'Thumbnail of a PNG equal to the original size needs original' );
+ assert.ok( provider.needsOriginal( file, 2000, 1000 ), 'Thumbnail of a PNG bigger than the original size needs original' );
+ } );
+
+ QUnit.test( 'isFullSizeUrl()', function ( assert ) {
+ var provider = new mw.mmv.provider.GuessedThumbnailInfo(),
+ file = new mw.Title( 'File:Copyleft.svg' );
+
+ assert.ok( !provider.isFullSizeUrl( 'http://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/Copyleft.svg/300px-Copyleft.svg.png', file ),
+ 'Thumbnail url recognized as not being full size' );
+ assert.ok( provider.isFullSizeUrl( 'http://upload.wikimedia.org/wikipedia/commons/8/8b/Copyleft.svg', file ),
+ 'Original url recognized as being full size' );
+ } );
+
+ QUnit.test( 'obscureFilename()', function ( assert ) {
+ var provider = new mw.mmv.provider.GuessedThumbnailInfo(),
+ file = new mw.Title( 'File:Copyleft.svg' );
+
+ assert.strictEqual( provider.obscureFilename( 'http://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/Copyleft.svg/300px-Copyleft.svg.png', file ),
+ 'http://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/<filename>/300px-<filename>.png', 'Filename correctly obscured' );
+
+ file = new mw.Title( 'File:Hoag\'s_object.jpg' );
+
+ assert.strictEqual( provider.obscureFilename( 'http://upload.wikimedia.org/wikipedia/commons/thumb/d/da/Hoag%27s_object.jpg/180px-Hoag%27s_object.jpg', file ),
+ 'http://upload.wikimedia.org/wikipedia/commons/thumb/d/da/<filename>/180px-<filename>', 'Filename with urlencoded character correctly obscured' );
+ } );
+
+ QUnit.test( 'restoreFilename()', function ( assert ) {
+ var provider = new mw.mmv.provider.GuessedThumbnailInfo(),
+ file = new mw.Title( 'File:Copyleft.svg' );
+
+ assert.strictEqual( provider.restoreFilename( 'http://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/<filename>/300px-<filename>.png', file ),
+ 'http://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/Copyleft.svg/300px-Copyleft.svg.png', 'Filename correctly restored' );
+
+ } );
+
+ QUnit.test( 'canHaveLargerThumbnailThanOriginal()', function ( assert ) {
+ var provider = new mw.mmv.provider.GuessedThumbnailInfo(),
+ file = new mw.Title( 'File:Copyleft.svg' );
+
+ assert.ok( provider.canHaveLargerThumbnailThanOriginal( file ), 'SVG can have a larger thumbnail than the original' );
+
+ file = new mw.Title( 'File:Foo.jpg' );
+
+ assert.ok( !provider.canHaveLargerThumbnailThanOriginal( file ), 'JPG can\'t have a larger thumbnail than the original' );
+
+ file = new mw.Title( 'File:Foo.png' );
+
+ assert.ok( !provider.canHaveLargerThumbnailThanOriginal( file ), 'PNG can\'t have a larger thumbnail than the original' );
+
+ file = new mw.Title( 'File:Foo.jpeg' );
+
+ assert.ok( !provider.canHaveLargerThumbnailThanOriginal( file ), 'JPEG can\'t have a larger thumbnail than the original' );
+
+ file = new mw.Title( 'File:Foo.tiff' );
+
+ assert.ok( !provider.canHaveLargerThumbnailThanOriginal( file ), 'TIFF can\'t have a larger thumbnail than the original' );
+
+ file = new mw.Title( 'File:Foo.gif' );
+
+ assert.ok( !provider.canHaveLargerThumbnailThanOriginal( file ), 'GIF can\'t have a larger thumbnail than the original' );
+ } );
+
+ QUnit.test( 'canBeDisplayedInBrowser()', function ( assert ) {
+ var provider = new mw.mmv.provider.GuessedThumbnailInfo(),
+ file = new mw.Title( 'File:Copyleft.svg' );
+
+ assert.ok( !provider.canBeDisplayedInBrowser( file ), 'SVG can\'t be displayed as-is in the browser' );
+
+ file = new mw.Title( 'File:Foo.jpg' );
+
+ assert.ok( provider.canBeDisplayedInBrowser( file ), 'JPG can be displayed as-is in the browser' );
+
+ file = new mw.Title( 'File:Foo.png' );
+
+ assert.ok( provider.canBeDisplayedInBrowser( file ), 'PNG can be displayed as-is in the browser' );
+
+ file = new mw.Title( 'File:Foo.jpeg' );
+
+ assert.ok( provider.canBeDisplayedInBrowser( file ), 'JPEG can be displayed as-is in the browser' );
+
+ file = new mw.Title( 'File:Foo.tiff' );
+
+ assert.ok( !provider.canBeDisplayedInBrowser( file ), 'TIFF can\'t be displayed as-is in the browser' );
+
+ file = new mw.Title( 'File:Foo.gif' );
+
+ assert.ok( provider.canBeDisplayedInBrowser( file ), 'GIF can be displayed as-is in the browser' );
+ } );
+
+ QUnit.test( 'guessWidth()', function ( assert ) {
+ var provider = new mw.mmv.provider.GuessedThumbnailInfo(),
+ file = new mw.Title( 'File:Copyleft.svg' );
+
+ assert.strictEqual( provider.guessWidth( file, 100, 1000 ), 100, 'Width correctly guessed for SVG thumbnail smaller than the original' );
+ assert.strictEqual( provider.guessWidth( file, 2000, 1000 ), 2000, 'Width correctly guessed for SVG thumbnail bigger than the original' );
+
+ file = new mw.Title( 'File:Copyleft.jpg' );
+
+ assert.strictEqual( provider.guessWidth( file, 100, 1000 ), 100, 'Width correctly guessed for JPG thumbnail smaller than the original' );
+ assert.strictEqual( provider.guessWidth( file, 2000, 1000 ), 1000, 'Width correctly guessed for JPG thumbnail bigger than the original' );
+ } );
+
+ QUnit.test( 'guessHeight()', function ( assert ) {
+ var provider = new mw.mmv.provider.GuessedThumbnailInfo(),
+ file = new mw.Title( 'File:Copyleft.svg' );
+
+ assert.strictEqual( provider.guessHeight( file, 100, 1000, 500 ), 50, 'Height correctly guessed for SVG thumbnail smaller than the original' );
+ assert.strictEqual( provider.guessHeight( file, 2000, 1000, 500 ), 1000, 'Height correctly guessed for SVG thumbnail bigger than the original' );
+
+ file = new mw.Title( 'File:Copyleft.jpg' );
+
+ assert.strictEqual( provider.guessHeight( file, 100, 1000, 500 ), 50, 'Height correctly guessed for JPG thumbnail smaller than the original' );
+ assert.strictEqual( provider.guessHeight( file, 2000, 1000, 500 ), 500, 'Height correctly guessed for JPG thumbnail bigger than the original' );
+ } );
+
+ QUnit.test( 'replaceSize()', function ( assert ) {
+ var provider = new mw.mmv.provider.GuessedThumbnailInfo(),
+ file = new mw.Title( 'File:Copyleft.svg' );
+
+ assert.strictEqual( provider.replaceSize( file, 'http://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/Copyleft.svg/300px-Copyleft.svg.png', 220 ),
+ 'http://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/Copyleft.svg/220px-Copyleft.svg.png', 'Incorrect size correctly replaced' );
+ assert.strictEqual( provider.replaceSize( file, 'http://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/Copyleft.svg/300px-Copyleft.svg.png', 300 ),
+ 'http://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/Copyleft.svg/300px-Copyleft.svg.png', 'Identical size correctly left the same' );
+ assert.strictEqual( provider.replaceSize( file, 'http://upload.wikimedia.org/wikipedia/commons/8/8b/Copyleft.svg', 220 ),
+ undefined, 'Returns undefined when it cannot handle the URL' );
+
+ file = new mw.Title( 'File:Copyleft-300px.svg' );
+ assert.strictEqual( provider.replaceSize( file, 'http://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/Copyleft-300px.svg/300px-Copyleft-300px.svg.png', 220 ),
+ 'http://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/Copyleft-300px.svg/220px-Copyleft-300px.svg.png', 'Works with strange filename' );
+
+ file = new mw.Title( 'File:Ranunculus_gmelinii_NRCS-2.tiff' );
+ assert.strictEqual( provider.replaceSize( file, 'https://upload.wikimedia.org/wikipedia/commons/thumb/2/27/Ranunculus_gmelinii_NRCS-2.tiff/lossy-page1-428px-Ranunculus_gmelinii_NRCS-2.tiff.jpg', 220 ),
+ 'https://upload.wikimedia.org/wikipedia/commons/thumb/2/27/Ranunculus_gmelinii_NRCS-2.tiff/lossy-page1-220px-Ranunculus_gmelinii_NRCS-2.tiff.jpg', 'Works with extra parameters' );
+ } );
+
+ QUnit.test( 'guessFullUrl()', function ( assert ) {
+ var provider = new mw.mmv.provider.GuessedThumbnailInfo(),
+ file = new mw.Title( 'File:Copyleft.svg' ),
+ fullUrl = 'http://upload.wikimedia.org/wikipedia/commons/8/8b/Copyleft.svg',
+ sampleUrl = 'http://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/Copyleft.svg/300px-Copyleft.svg.png',
+ result;
+
+ result = provider.guessFullUrl( file, sampleUrl );
+
+ assert.strictEqual( result, fullUrl, 'guessFullUrl returns correct full URL for SVG' );
+
+ file = new mw.Title( 'File:அணில்-3-தென்னையின்_வளர்நிலை.jpg' );
+ fullUrl = 'https://upload.wikimedia.org/wikipedia/commons/1/15/%E0%AE%85%E0%AE%A3%E0%AE%BF%E0%AE%B2%E0%AF%8D-3-%E0%AE%A4%E0%AF%86%E0%AE%A9%E0%AF%8D%E0%AE%A9%E0%AF%88%E0%AE%AF%E0%AE%BF%E0%AE%A9%E0%AF%8D_%E0%AE%B5%E0%AE%B3%E0%AE%B0%E0%AF%8D%E0%AE%A8%E0%AE%BF%E0%AE%B2%E0%AF%88.jpg';
+ sampleUrl = 'https://upload.wikimedia.org/wikipedia/commons/thumb/1/15/%E0%AE%85%E0%AE%A3%E0%AE%BF%E0%AE%B2%E0%AF%8D-3-%E0%AE%A4%E0%AF%86%E0%AE%A9%E0%AF%8D%E0%AE%A9%E0%AF%88%E0%AE%AF%E0%AE%BF%E0%AE%A9%E0%AF%8D_%E0%AE%B5%E0%AE%B3%E0%AE%B0%E0%AF%8D%E0%AE%A8%E0%AE%BF%E0%AE%B2%E0%AF%88.jpg/800px-%E0%AE%85%E0%AE%A3%E0%AE%BF%E0%AE%B2%E0%AF%8D-3-%E0%AE%A4%E0%AF%86%E0%AE%A9%E0%AF%8D%E0%AE%A9%E0%AF%88%E0%AE%AF%E0%AE%BF%E0%AE%A9%E0%AF%8D_%E0%AE%B5%E0%AE%B3%E0%AE%B0%E0%AF%8D%E0%AE%A8%E0%AE%BF%E0%AE%B2%E0%AF%88.jpg';
+
+ result = provider.guessFullUrl( file, sampleUrl );
+
+ assert.strictEqual( result, fullUrl, 'guessFullUrl returns correct full URL for JPG with unicode name' );
+
+ file = new mw.Title( 'File:அணில்-3-தென்னையின்_வளர்நிலை.jpg' );
+ sampleUrl = 'https://upload.wikimedia.org/wikipedia/commons/thumb/1/15/அணில்-3-தென்னையின்_வளர்நிலை.jpg/800px-அணில்-3-தென்னையின்_வளர்நிலை.jpg';
+
+ result = provider.guessFullUrl( file, sampleUrl );
+
+ assert.strictEqual( result, undefined, 'guessFullUrl bails out when URL encoding is not as expected' );
+ } );
+}( mediaWiki ) );
diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/provider/mmv.provider.Image.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/provider/mmv.provider.Image.test.js
new file mode 100644
index 00000000..76b84afe
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/provider/mmv.provider.Image.test.js
@@ -0,0 +1,200 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $ ) {
+ QUnit.module( 'mmv.provider.Image', QUnit.newMwEnvironment() );
+
+ QUnit.test( 'Image constructor sanity check', function ( assert ) {
+ var imageProvider = new mw.mmv.provider.Image();
+
+ assert.ok( imageProvider );
+ } );
+
+ QUnit.test( 'Image load success', function ( assert ) {
+ var url = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0' +
+ 'iAAAABlBMVEUAAAD///+l2Z/dAAAAM0lEQVR4nGP4/5/h/1+G/58ZDrAz3D/McH' +
+ '8yw83NDDeNGe4Ug9C9zwz3gVLMDA/A6P9/AFGGFyjOXZtQAAAAAElFTkSuQmCC',
+ imageProvider = new mw.mmv.provider.Image();
+
+ imageProvider.imagePreloadingSupported = function () { return false; };
+ imageProvider.performance.recordEntry = $.noop;
+
+ return imageProvider.get( url ).then( function ( image ) {
+ assert.ok( image instanceof HTMLImageElement,
+ 'success handler was called with the image element' );
+ assert.strictEqual( image.src, url, 'image src is correct' );
+ } );
+ } );
+
+ QUnit.test( 'Image caching', function ( assert ) {
+ var url = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0' +
+ 'iAAAABlBMVEUAAAD///+l2Z/dAAAAM0lEQVR4nGP4/5/h/1+G/58ZDrAz3D/McH' +
+ '8yw83NDDeNGe4Ug9C9zwz3gVLMDA/A6P9/AFGGFyjOXZtQAAAAAElFTkSuQmCC',
+ url2 = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==',
+ result,
+ imageProvider = new mw.mmv.provider.Image();
+
+ imageProvider.imagePreloadingSupported = function () { return false; };
+ imageProvider.performance.recordEntry = $.noop;
+
+ return QUnit.whenPromisesComplete(
+ imageProvider.get( url ).then( function ( image ) {
+ result = image;
+ assert.ok( image instanceof HTMLImageElement,
+ 'success handler was called with the image element' );
+ assert.strictEqual( image.src, url, 'image src is correct' );
+ } ),
+
+ imageProvider.get( url ).then( function ( image ) {
+ assert.strictEqual( image, result, 'image element is cached and not regenerated' );
+ assert.strictEqual( image.src, url, 'image src is correct' );
+ } ),
+
+ imageProvider.get( url2 ).then( function ( image ) {
+ assert.notStrictEqual( image, result, 'image element for different url is not cached' );
+ assert.strictEqual( image.src, url2, 'image src is correct' );
+ } )
+ );
+ } );
+
+ QUnit.test( 'Image load XHR progress funneling', function ( assert ) {
+ var i = 0,
+ imageProvider = new mw.mmv.provider.Image(),
+ oldPerformance = imageProvider.performance,
+ fakeURL = 'fakeURL',
+ response = 'response',
+ done1 = assert.async(),
+ done2 = assert.async();
+
+ imageProvider.performance.delay = 0;
+ imageProvider.imagePreloadingSupported = function () { return true; };
+ imageProvider.rawGet = function () { return $.Deferred().resolve(); };
+
+ imageProvider.performance.newXHR = function () {
+ return { readyState: 4,
+ response: response,
+ send: function () {
+ var self = this;
+
+ // The timeout is necessary because without it notify() happens before
+ // the imageProvider has time to chain its progress() to the returned deferred
+ setTimeout( function () {
+ self.onprogress( { lengthComputable: true, loaded: 10, total: 20 } );
+ self.onreadystatechange();
+ } );
+ },
+
+ open: $.noop };
+ };
+
+ imageProvider.performance.recordEntry = function ( type, total, url ) {
+ assert.strictEqual( type, 'image', 'Type matches' );
+ assert.strictEqual( url, fakeURL, 'URL matches' );
+ done1();
+
+ imageProvider.performance = oldPerformance;
+
+ return $.Deferred().resolve();
+ };
+
+ imageProvider.get( fakeURL )
+ .fail( function () {
+ assert.ok( false, 'Image failed to (pretend to) load' );
+ done2();
+ } )
+ .then( function () {
+ assert.ok( true, 'Image was pretend-loaded' );
+ done2();
+ } )
+ .progress( function ( response, percent ) {
+ if ( i === 0 ) {
+ assert.strictEqual( percent, 50, 'Correctly propagated a 50% progress event' );
+ assert.strictEqual( response, response, 'Partial response propagated' );
+ } else if ( i === 1 ) {
+ assert.strictEqual( percent, 100, 'Correctly propagated a 100% progress event' );
+ assert.strictEqual( response, response, 'Partial response propagated' );
+ } else {
+ assert.ok( false, 'Only 2 progress events should propagate' );
+ }
+
+ i++;
+ } );
+ } );
+
+ QUnit.test( 'Image load fail', function ( assert ) {
+ var imageProvider = new mw.mmv.provider.Image(),
+ oldMwLog = mw.log,
+ done = assert.async(),
+ mwLogCalled = false;
+
+ imageProvider.imagePreloadingSupported = function () { return false; };
+ imageProvider.performance.recordEntry = $.noop;
+ mw.log = function () { mwLogCalled = true; };
+
+ imageProvider.get( 'doesntexist.png' ).fail( function () {
+ assert.ok( true, 'fail handler was called' );
+ assert.ok( mwLogCalled, 'mw.log was called' );
+ mw.log = oldMwLog;
+ done();
+ } );
+ } );
+
+ QUnit.test( 'Image load with preloading supported', function ( assert ) {
+ var url = mw.config.get( 'wgExtensionAssetsPath' ) + '/MultimediaViewer/resources/mmv/img/expand.svg',
+ imageProvider = new mw.mmv.provider.Image(),
+ endsWith = function ( a, b ) { return a.indexOf( b ) === a.length - b.length; };
+
+ imageProvider.imagePreloadingSupported = function () { return true; };
+ imageProvider.performance = {
+ record: function () { return $.Deferred().resolve(); }
+ };
+
+ return imageProvider.get( url ).then( function ( image ) {
+ // can't test equality as browsers transform this to a full URL
+ assert.ok( endsWith( image.src, url ), 'local image loaded with correct source' );
+ } );
+ } );
+
+ QUnit.test( 'Failed image load with preloading supported', function ( assert ) {
+ var url = 'nosuchimage.png',
+ imageProvider = new mw.mmv.provider.Image(),
+ done = assert.async();
+
+ imageProvider.imagePreloadingSupported = function () { return true; };
+ imageProvider.performance = {
+ record: function () { return $.Deferred().resolve(); }
+ };
+
+ imageProvider.get( url ).fail( function () {
+ assert.ok( true, 'Fail callback called for non-existing image' );
+ done();
+ } );
+ } );
+
+ QUnit.test( 'imageQueryParameter', function ( assert ) {
+ var imageProvider = new mw.mmv.provider.Image( 'foo' );
+
+ imageProvider.imagePreloadingSupported = function () { return false; };
+ imageProvider.rawGet = function () { return $.Deferred().resolve(); };
+
+ imageProvider.performance.recordEntry = function ( type, total, url ) {
+ assert.strictEqual( url, 'http://www.wikipedia.org/?foo', 'Extra parameter added' );
+ };
+
+ imageProvider.get( 'http://www.wikipedia.org/' );
+ } );
+}( mediaWiki, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/provider/mmv.provider.ImageInfo.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/provider/mmv.provider.ImageInfo.test.js
new file mode 100644
index 00000000..3daaac12
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/provider/mmv.provider.ImageInfo.test.js
@@ -0,0 +1,241 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $ ) {
+ QUnit.module( 'mmv.provider.ImageInfo', QUnit.newMwEnvironment() );
+
+ QUnit.test( 'ImageInfo constructor sanity check', function ( assert ) {
+ var api = { get: function () {} },
+ imageInfoProvider = new mw.mmv.provider.ImageInfo( api );
+
+ assert.ok( imageInfoProvider );
+ } );
+
+ QUnit.test( 'ImageInfo get test', function ( assert ) {
+ var apiCallCount = 0,
+ api = { get: function () {
+ apiCallCount++;
+ return $.Deferred().resolve( {
+ query: {
+ pages: {
+ '-1': {
+ ns: 6,
+ title: 'File:Stuff.jpg',
+ missing: '',
+ imagerepository: 'shared',
+ imageinfo: [
+ {
+ timestamp: '2013-08-25T14:41:02Z',
+ userid: '3053121',
+ size: 346684,
+ width: 720,
+ height: 1412,
+ comment: 'User created page with UploadWizard',
+ url: 'https://upload.wikimedia.org/wikipedia/commons/1/19/Stuff.jpg',
+ descriptionurl: 'https://commons.wikimedia.org/wiki/File:Stuff.jpg',
+ sha1: 'a1ba23d471f4dad208b71c143e2e105a0e3032db',
+ metadata: [],
+ extmetadata: {
+ ObjectName: {
+ value: 'Some stuff',
+ source: 'commons-templates'
+ },
+ License: {
+ value: 'cc0',
+ source: 'commons-templates',
+ hidden: ''
+ },
+ LicenseShortName: {
+ value: 'CC0',
+ source: 'commons-templates'
+ },
+ UsageTerms: {
+ value: 'Creative Commons Public Domain Dedication',
+ source: 'commons-templates'
+ },
+ LicenseUrl: {
+ value: 'http://creativecommons.org/publicdomain/zero/1.0/',
+ source: 'commons-templates'
+ },
+ GPSLatitude: {
+ value: '90.000000',
+ source: 'commons-desc-page'
+ },
+ GPSLongitude: {
+ value: ' 180.000000',
+ source: 'commons-desc-page'
+ },
+ ImageDescription: {
+ value: 'Wikis stuff',
+ source: 'commons-desc-page'
+ },
+ DateTimeOriginal: {
+ value: '<time class="dtstart" datetime="2009-02-18">18 February 2009</time>\u00a0(according to <a href="//en.wikipedia.org/wiki/Exchangeable_image_file_format" class="extiw" title="en:Exchangeable image file format">EXIF</a> data)',
+ source: 'commons-desc-page'
+ },
+ DateTime: {
+ value: '2013-08-25T14:41:02Z',
+ source: 'commons-desc-page'
+ },
+ Credit: {
+ value: 'Wikipedia',
+ source: 'commons-desc-page',
+ hidden: ''
+ },
+ Artist: {
+ value: 'John Smith',
+ source: 'commons-desc-page'
+ },
+ AuthorCount: {
+ value: '2',
+ source: 'commons-desc-page'
+ },
+ Attribution: {
+ value: 'By John Smith',
+ source: 'commons-desc-page'
+ },
+ Permission: {
+ value: 'Do not use. Ever.',
+ source: 'commons-desc-page'
+ },
+ AttributionRequired: {
+ value: 'no',
+ source: 'commons-desc-page'
+ },
+ NonFree: {
+ value: 'yes',
+ source: 'commons-desc-page'
+ },
+ Restrictions: {
+ value: 'trademarked|insignia',
+ source: 'commons-desc-page'
+ },
+ DeletionReason: {
+ value: 'copyvio',
+ source: 'commons-desc-page'
+ }
+ },
+ mime: 'image/jpeg',
+ mediatype: 'BITMAP'
+ }
+ ]
+ }
+ }
+ }
+ } );
+ } },
+ file = new mw.Title( 'File:Stuff.jpg' ),
+ imageInfoProvider = new mw.mmv.provider.ImageInfo( api );
+
+ return imageInfoProvider.get( file ).then( function ( image ) {
+ assert.strictEqual( image.title.getPrefixedDb(), 'File:Stuff.jpg', 'title is set correctly' );
+ assert.strictEqual( image.name, 'Some stuff', 'name is set correctly' );
+ assert.strictEqual( image.size, 346684, 'size is set correctly' );
+ assert.strictEqual( image.width, 720, 'width is set correctly' );
+ assert.strictEqual( image.height, 1412, 'height is set correctly' );
+ assert.strictEqual( image.mimeType, 'image/jpeg', 'mimeType is set correctly' );
+ assert.strictEqual( image.url, 'https://upload.wikimedia.org/wikipedia/commons/1/19/Stuff.jpg', 'url is set correctly' );
+ assert.strictEqual( image.descriptionUrl, 'https://commons.wikimedia.org/wiki/File:Stuff.jpg', 'descriptionUrl is set correctly' );
+ assert.strictEqual( image.repo, 'shared', 'repo is set correctly' );
+ assert.strictEqual( image.uploadDateTime, '2013-08-25T14:41:02Z', 'uploadDateTime is set correctly' );
+ assert.strictEqual( image.anonymizedUploadDateTime, '20130825000000', 'anonymizedUploadDateTime is set correctly' );
+ assert.strictEqual( image.creationDateTime, '18 February 2009\u00a0(according to EXIF data)', 'creationDateTime is set correctly' );
+ assert.strictEqual( image.description, 'Wikis stuff', 'description is set correctly' );
+ assert.strictEqual( image.source, 'Wikipedia', 'source is set correctly' );
+ assert.strictEqual( image.author, 'John Smith', 'author is set correctly' );
+ assert.strictEqual( image.authorCount, 2, 'author count is set correctly' );
+ assert.strictEqual( image.attribution, 'By John Smith', 'attribution is set correctly' );
+ assert.strictEqual( image.license.shortName, 'CC0', 'license short name is set correctly' );
+ assert.strictEqual( image.license.internalName, 'cc0', 'license internal name is set correctly' );
+ assert.strictEqual( image.license.longName, 'Creative Commons Public Domain Dedication', 'license long name is set correctly' );
+ assert.strictEqual( image.license.deedUrl, 'http://creativecommons.org/publicdomain/zero/1.0/', 'license URL is set correctly' );
+ assert.strictEqual( image.license.attributionRequired, false, 'Attribution required flag is honored' );
+ assert.strictEqual( image.license.nonFree, true, 'Non-free flag is honored' );
+ assert.strictEqual( image.permission, 'Do not use. Ever.', 'permission is set correctly' );
+ assert.strictEqual( image.deletionReason, 'copyvio', 'permission is set correctly' );
+ assert.strictEqual( image.latitude, 90, 'latitude is set correctly' );
+ assert.strictEqual( image.longitude, 180, 'longitude is set correctly' );
+ assert.deepEqual( image.restrictions, [ 'trademarked', 'insignia' ], 'restrictions is set correctly' );
+ } ).then( function () {
+ // call the data provider a second time to check caching
+ return imageInfoProvider.get( file );
+ } ).then( function () {
+ assert.strictEqual( apiCallCount, 1 );
+ } );
+ } );
+
+ QUnit.test( 'ImageInfo fail test', function ( assert ) {
+ var api = { get: function () {
+ return $.Deferred().resolve( {} );
+ } },
+ file = new mw.Title( 'File:Stuff.jpg' ),
+ done = assert.async(),
+ imageInfoProvider = new mw.mmv.provider.ImageInfo( api );
+
+ imageInfoProvider.get( file ).fail( function () {
+ assert.ok( true, 'promise rejected when no data is returned' );
+ done();
+ } );
+ } );
+
+ QUnit.test( 'ImageInfo fail test 2', function ( assert ) {
+ var api = { get: function () {
+ return $.Deferred().resolve( {
+ query: {
+ pages: {
+ '-1': {
+ title: 'File:Stuff.jpg'
+ }
+ }
+ }
+ } );
+ } },
+ file = new mw.Title( 'File:Stuff.jpg' ),
+ done = assert.async(),
+ imageInfoProvider = new mw.mmv.provider.ImageInfo( api );
+
+ imageInfoProvider.get( file ).fail( function () {
+ assert.ok( true, 'promise rejected when imageinfo is missing' );
+ done();
+ } );
+ } );
+
+ QUnit.test( 'ImageInfo missing page test', function ( assert ) {
+ var api = { get: function () {
+ return $.Deferred().resolve( {
+ query: {
+ pages: {
+ '-1': {
+ title: 'File:Stuff.jpg',
+ missing: '',
+ imagerepository: ''
+ }
+ }
+ }
+ } );
+ } },
+ file = new mw.Title( 'File:Stuff.jpg' ),
+ done = assert.async(),
+ imageInfoProvider = new mw.mmv.provider.ImageInfo( api );
+
+ imageInfoProvider.get( file ).fail( function ( errorMessage ) {
+ assert.strictEqual( errorMessage, 'file does not exist: File:Stuff.jpg',
+ 'error message is set correctly for missing file' );
+ done();
+ } );
+ } );
+}( mediaWiki, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/provider/mmv.provider.ThumbnailInfo.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/provider/mmv.provider.ThumbnailInfo.test.js
new file mode 100644
index 00000000..19a18e6a
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/provider/mmv.provider.ThumbnailInfo.test.js
@@ -0,0 +1,165 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $ ) {
+ QUnit.module( 'mmv.provider.ThumbnailInfo', QUnit.newMwEnvironment() );
+
+ QUnit.test( 'ThumbnailInfo constructor sanity check', function ( assert ) {
+ var api = { get: function () {} },
+ thumbnailInfoProvider = new mw.mmv.provider.ThumbnailInfo( api );
+
+ assert.ok( thumbnailInfoProvider );
+ } );
+
+ QUnit.test( 'ThumbnailInfo get test', function ( assert ) {
+ var apiCallCount = 0,
+ api = { get: function () {
+ apiCallCount++;
+ return $.Deferred().resolve( {
+ query: {
+ pages: {
+ '-1': {
+ ns: 6,
+ title: 'File:Stuff.jpg',
+ missing: '',
+ imagerepository: 'shared',
+ imageinfo: [
+ {
+ thumburl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/1/19/Stuff.jpg/51px-Stuff.jpg',
+ thumbwidth: 95,
+ thumbheight: 200,
+ url: 'https://upload.wikimedia.org/wikipedia/commons/1/19/Stuff.jpg',
+ descriptionurl: 'https://commons.wikimedia.org/wiki/File:Stuff.jpg'
+ }
+ ]
+ }
+ }
+ }
+ } );
+ } },
+ file = new mw.Title( 'File:Stuff.jpg' ),
+ thumbnailInfoProvider = new mw.mmv.provider.ThumbnailInfo( api );
+
+ return thumbnailInfoProvider.get( file, 100 ).then( function ( thumbnail ) {
+ assert.strictEqual( thumbnail.url,
+ 'https://upload.wikimedia.org/wikipedia/commons/thumb/1/19/Stuff.jpg/51px-Stuff.jpg',
+ 'URL is set correctly' );
+ assert.strictEqual( thumbnail.width, 95, 'actual width is set correctly' );
+ assert.strictEqual( thumbnail.height, 200, 'actual height is set correctly' );
+ } ).then( function () {
+ assert.strictEqual( apiCallCount, 1 );
+ // call the data provider a second time to check caching
+ return thumbnailInfoProvider.get( file, 100 );
+ } ).then( function () {
+ assert.strictEqual( apiCallCount, 1 );
+ // call a third time with different size to check caching
+ return thumbnailInfoProvider.get( file, 110 );
+ } ).then( function () {
+ assert.strictEqual( apiCallCount, 2 );
+ // call it again, with a height specified, to check caching
+ return thumbnailInfoProvider.get( file, 110, 100 );
+ } ).then( function () {
+ assert.strictEqual( apiCallCount, 3 );
+ } );
+ } );
+
+ QUnit.test( 'ThumbnailInfo fail test', function ( assert ) {
+ var api = { get: function () {
+ return $.Deferred().resolve( {} );
+ } },
+ file = new mw.Title( 'File:Stuff.jpg' ),
+ done = assert.async(),
+ thumbnailInfoProvider = new mw.mmv.provider.ThumbnailInfo( api );
+
+ thumbnailInfoProvider.get( file, 100 ).fail( function () {
+ assert.ok( true, 'promise rejected when no data is returned' );
+ done();
+ } );
+ } );
+
+ QUnit.test( 'ThumbnailInfo fail test 2', function ( assert ) {
+ var api = { get: function () {
+ return $.Deferred().resolve( {
+ query: {
+ pages: {
+ '-1': {
+ title: 'File:Stuff.jpg'
+ }
+ }
+ }
+ } );
+ } },
+ file = new mw.Title( 'File:Stuff.jpg' ),
+ done = assert.async(),
+ thumbnailInfoProvider = new mw.mmv.provider.ThumbnailInfo( api );
+
+ thumbnailInfoProvider.get( file, 100 ).fail( function () {
+ assert.ok( true, 'promise rejected when imageinfo is missing' );
+ done();
+ } );
+ } );
+
+ QUnit.test( 'ThumbnailInfo missing page test', function ( assert ) {
+ var api = { get: function () {
+ return $.Deferred().resolve( {
+ query: {
+ pages: {
+ '-1': {
+ title: 'File:Stuff.jpg',
+ missing: '',
+ imagerepository: ''
+ }
+ }
+ }
+ } );
+ } },
+ file = new mw.Title( 'File:Stuff.jpg' ),
+ done = assert.async(),
+ thumbnailInfoProvider = new mw.mmv.provider.ThumbnailInfo( api );
+
+ thumbnailInfoProvider.get( file ).fail( function ( errorMessage ) {
+ assert.strictEqual( errorMessage, 'file does not exist: File:Stuff.jpg',
+ 'error message is set correctly for missing file' );
+ done();
+ } );
+ } );
+
+ QUnit.test( 'ThumbnailInfo fail test 3', function ( assert ) {
+ var api = { get: function () {
+ return $.Deferred().resolve( {
+ query: {
+ pages: {
+ '-1': {
+ title: 'File:Stuff.jpg',
+ imageinfo: [
+ {}
+ ]
+ }
+ }
+ }
+ } );
+ } },
+ file = new mw.Title( 'File:Stuff.jpg' ),
+ done = assert.async(),
+ thumbnailInfoProvider = new mw.mmv.provider.ThumbnailInfo( api );
+
+ thumbnailInfoProvider.get( file, 100 ).fail( function () {
+ assert.ok( true, 'promise rejected when thumbnail info is missing' );
+ done();
+ } );
+ } );
+}( mediaWiki, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/routing/mmv.routing.MainFileRoute.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/routing/mmv.routing.MainFileRoute.test.js
new file mode 100644
index 00000000..49fcff6a
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/routing/mmv.routing.MainFileRoute.test.js
@@ -0,0 +1,24 @@
+/*
+ * This file is part of the MediaWiki extension MediaViewer.
+ *
+ * MediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw ) {
+ QUnit.module( 'mmv.routing.MainFileRoute', QUnit.newMwEnvironment() );
+
+ QUnit.test( 'Constructor sanity checks', function ( assert ) {
+ assert.ok( new mw.mmv.routing.MainFileRoute(), 'MainFileRoute created successfully' );
+ } );
+}( mediaWiki ) );
diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/routing/mmv.routing.Router.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/routing/mmv.routing.Router.test.js
new file mode 100644
index 00000000..3da76de5
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/routing/mmv.routing.Router.test.js
@@ -0,0 +1,232 @@
+/*
+ * This file is part of the MediaWiki extension MediaViewer.
+ *
+ * MediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw ) {
+ QUnit.module( 'mmv.routing.Router', QUnit.newMwEnvironment() );
+
+ QUnit.test( 'Constructor sanity checks', function ( assert ) {
+ var router;
+
+ router = new mw.mmv.routing.Router();
+ assert.ok( router, 'Router created successfully' );
+ } );
+
+ QUnit.test( 'isMediaViewerHash()', function ( assert ) {
+ var router = new mw.mmv.routing.Router();
+
+ assert.ok( router.isMediaViewerHash( 'mediaviewer/foo' ), 'Legacy hash' );
+ assert.ok( router.isMediaViewerHash( '#mediaviewer/foo' ), 'Legacy hash with #' );
+ assert.ok( router.isMediaViewerHash( 'mediaviewer' ), 'Bare legacy hash' );
+ assert.ok( router.isMediaViewerHash( '#mediaviewer' ), 'Bare legacy hash with #' );
+ assert.ok( router.isMediaViewerHash( '/media/foo' ), 'Normal hash' );
+ assert.ok( router.isMediaViewerHash( '#/media/foo' ), 'Normal hash with #' );
+ assert.ok( router.isMediaViewerHash( '/media' ), 'Bare hash' );
+ assert.ok( router.isMediaViewerHash( '#/media' ), 'Bare hash with #' );
+ assert.ok( !router.isMediaViewerHash( 'foo/media' ), 'Foreign hash' );
+ assert.ok( !router.isMediaViewerHash( '' ), 'Empty hash' );
+ } );
+
+ QUnit.test( 'createHash()/parseHash()', function ( assert ) {
+ var route, parsedRoute, hash, title,
+ router = new mw.mmv.routing.Router();
+
+ route = new mw.mmv.routing.MainFileRoute();
+ hash = router.createHash( route );
+ parsedRoute = router.parseHash( hash );
+ assert.deepEqual( parsedRoute, route, 'Bare hash' );
+
+ title = new mw.Title( 'File:Foo.png' );
+ route = new mw.mmv.routing.ThumbnailRoute( title );
+ hash = router.createHash( route );
+ parsedRoute = router.parseHash( hash );
+ assert.strictEqual( parsedRoute.fileTitle.getPrefixedDb(),
+ title.getPrefixedDb(), 'Normal hash' );
+ assert.ok( hash.match( /File:Foo.png/ ), 'Simple filenames remain readable' );
+
+ title = new mw.Title( 'File:Foo.png' );
+ route = new mw.mmv.routing.ThumbnailRoute( title );
+ hash = router.createHash( route );
+ assert.notEqual( hash[ 0 ], '#', 'Leading # is not included in the returned hash' );
+ parsedRoute = router.parseHash( '#' + hash );
+ assert.strictEqual( parsedRoute.fileTitle.getPrefixedDb(),
+ title.getPrefixedDb(), 'Leading # is accepted when parsing a hash' );
+
+ title = new mw.Title( 'File:Foo.png' );
+ route = new mw.mmv.routing.ThumbnailRoute( title );
+ hash = router.createHash( route );
+ parsedRoute = router.parseHash( hash );
+ assert.strictEqual( parsedRoute.fileTitle.getPrefixedDb(),
+ title.getPrefixedDb(), 'Normal hash' );
+ assert.ok( hash.match( /File:Foo.png/ ), 'Simple filenames remain readable' );
+
+ title = new mw.Title( 'File:Foo/bar.png' );
+ route = new mw.mmv.routing.ThumbnailRoute( title );
+ hash = router.createHash( route );
+ parsedRoute = router.parseHash( hash );
+ assert.strictEqual( parsedRoute.fileTitle.getPrefixedDb(),
+ title.getPrefixedDb(), 'Filename with /' );
+ assert.ok( !hash.match( 'Foo/bar' ), '/ is encoded' );
+
+ title = new mw.Title( 'File:Foo bar.png' );
+ route = new mw.mmv.routing.ThumbnailRoute( title );
+ hash = router.createHash( route );
+ parsedRoute = router.parseHash( hash );
+ assert.strictEqual( parsedRoute.fileTitle.getPrefixedDb(),
+ title.getPrefixedDb(), 'Filename with space' );
+ assert.ok( !hash.match( 'Foo bar' ), 'space is replaced...' );
+ assert.ok( hash.match( 'Foo_bar' ), '...with underscore' );
+
+ title = new mw.Title( 'File:看門狗 (遊戲).jpg' );
+ route = new mw.mmv.routing.ThumbnailRoute( title );
+ hash = router.createHash( route );
+ parsedRoute = router.parseHash( hash );
+ assert.strictEqual( parsedRoute.fileTitle.getPrefixedDb(),
+ title.getPrefixedDb(), 'Unicode filename' );
+
+ title = new mw.Title( 'File:%!"$&\'()*,-./:;=?@\\^_`~+.jpg' );
+ if ( title ) {
+ route = new mw.mmv.routing.ThumbnailRoute( title );
+ hash = router.createHash( route );
+ parsedRoute = router.parseHash( hash );
+ assert.strictEqual( parsedRoute.fileTitle.getPrefixedDb(),
+ title.getPrefixedDb(), 'Special characters' );
+ } else {
+ // mw.Title depends on $wgLegalTitleChars - do not fail test if it is non-standard
+ assert.ok( true, 'Skipped' );
+ }
+ } );
+
+ QUnit.test( 'createHash() error handling', function ( assert ) {
+ var router = new mw.mmv.routing.Router();
+
+ assert.ok( mw.mmv.testHelpers.getException( function () { return new mw.mmv.routing.ThumbnailRoute(); } ),
+ 'Exception thrown then ThumbnailRoute has no title' );
+ assert.ok( mw.mmv.testHelpers.getException( function () {
+ router.createHash( this.sandbox.createStubInstance( mw.mmv.routing.Route ) );
+ } ), 'Exception thrown for unknown Route subclass' );
+ assert.ok( mw.mmv.testHelpers.getException( function () {
+ router.createHash( {} );
+ } ), 'Exception thrown for non-Route class' );
+ } );
+
+ QUnit.test( 'parseHash() with invalid hashes', function ( assert ) {
+ var router = new mw.mmv.routing.Router();
+
+ assert.ok( !router.parseHash( 'foo' ), 'Non-MMV hash is rejected.' );
+ assert.ok( !router.parseHash( '#foo' ), 'Non-MMV hash is rejected (with #).' );
+ assert.ok( !router.parseHash( '/media/foo/bar' ), 'Invalid MMV hash is rejected.' );
+ assert.ok( !router.parseHash( '#/media/foo/bar' ), 'Invalid MMV hash is rejected (with #).' );
+ } );
+
+ QUnit.test( 'parseHash() backwards compatibility', function ( assert ) {
+ var route,
+ router = new mw.mmv.routing.Router();
+
+ route = router.parseHash( '#mediaviewer/File:Foo bar.png' );
+ assert.strictEqual( route.fileTitle.getPrefixedDb(), 'File:Foo_bar.png',
+ 'Old urls (with space) are handled' );
+
+ route = router.parseHash( '#mediaviewer/File:Mexican \'Alien\' Piñata.jpg' );
+ assert.strictEqual( route.fileTitle.getPrefixedDb(), 'File:Mexican_\'Alien\'_Piñata.jpg',
+ 'Old urls (without percent-encoding) are handled' );
+ } );
+
+ QUnit.test( 'createHashedUrl()', function ( assert ) {
+ var url,
+ route = new mw.mmv.routing.MainFileRoute(),
+ router = new mw.mmv.routing.Router();
+
+ url = router.createHashedUrl( route, 'http://example.com/' );
+ assert.strictEqual( url, 'http://example.com/#/media', 'Url generation works' );
+
+ url = router.createHashedUrl( route, 'http://example.com/#foo' );
+ assert.strictEqual( url, 'http://example.com/#/media', 'Urls with fragments are handled' );
+ } );
+
+ QUnit.test( 'parseLocation()', function ( assert ) {
+ var location, route,
+ router = new mw.mmv.routing.Router();
+
+ location = { href: 'http://example.com/foo#mediaviewer/File:Foo.png' };
+ route = router.parseLocation( location );
+ assert.strictEqual( route.fileTitle.getPrefixedDb(), 'File:Foo.png', 'Reading location works' );
+
+ location = { href: 'http://example.com/foo#/media/File:Foo.png' };
+ route = router.parseLocation( location );
+ assert.strictEqual( route.fileTitle.getPrefixedDb(), 'File:Foo.png', 'Reading location works' );
+
+ location = { href: 'http://example.com/foo' };
+ route = router.parseLocation( location );
+ assert.ok( !route, 'Reading location without fragment part works' );
+ } );
+
+ QUnit.test( 'parseLocation() with real location', function ( assert ) {
+ var route, title, hash,
+ router = new mw.mmv.routing.Router();
+
+ // mw.Title does not accept % in page names
+ this.sandbox.stub( mw, 'Title', function ( name ) {
+ return {
+ name: name,
+ getMain: function () { return name.replace( /^File:/, '' ); }
+ };
+ } );
+ title = new mw.Title( 'File:%40.png' );
+ hash = router.createHash( new mw.mmv.routing.ThumbnailRoute( title ) );
+
+ window.location.hash = hash;
+ route = router.parseLocation( window.location );
+ assert.strictEqual( route.fileTitle.getMain(), '%40.png',
+ 'Reading location set via location.hash works' );
+
+ if ( window.history ) {
+ window.history.pushState( null, null, '#' + hash );
+ route = router.parseLocation( window.location );
+ assert.strictEqual( route.fileTitle.getMain(), '%40.png',
+ 'Reading location set via pushState() works' );
+ } else {
+ assert.ok( true, 'Skipped pushState() test, not supported on this browser' );
+ }
+
+ // reset location, might interfere with other tests
+ window.location.hash = '#';
+ } );
+
+ QUnit.test( 'tokenizeHash()', function ( assert ) {
+ var router = new mw.mmv.routing.Router();
+
+ router.legacyPrefix = 'legacy';
+ router.applicationPrefix = 'prefix';
+
+ assert.deepEqual( router.tokenizeHash( '#foo/bar' ), [], 'No known prefix' );
+
+ assert.deepEqual( router.tokenizeHash( '#prefix' ), [ 'prefix' ], 'Current prefix, with #' );
+ assert.deepEqual( router.tokenizeHash( 'prefix' ), [ 'prefix' ], 'Current prefix, without #' );
+ assert.deepEqual( router.tokenizeHash( '#prefix/bar' ), [ 'prefix', 'bar' ], 'Current prefix, with # and element' );
+ assert.deepEqual( router.tokenizeHash( 'prefix/bar' ), [ 'prefix', 'bar' ], 'Current prefix, with element without #' );
+ assert.deepEqual( router.tokenizeHash( '#prefix/bar/baz' ), [ 'prefix', 'bar', 'baz' ], 'Current prefix, with # and 2 elements' );
+ assert.deepEqual( router.tokenizeHash( 'prefix/bar/baz' ), [ 'prefix', 'bar', 'baz' ], 'Current prefix, with 2 elements without #' );
+
+ assert.deepEqual( router.tokenizeHash( '#legacy' ), [ 'legacy' ], 'Legacy prefix, with #' );
+ assert.deepEqual( router.tokenizeHash( 'legacy' ), [ 'legacy' ], 'Legacy prefix, without #' );
+ assert.deepEqual( router.tokenizeHash( '#legacy/bar' ), [ 'legacy', 'bar' ], 'Legacy prefix, with # and element' );
+ assert.deepEqual( router.tokenizeHash( 'legacy/bar' ), [ 'legacy', 'bar' ], 'Legacy prefix, with element without #' );
+ assert.deepEqual( router.tokenizeHash( '#legacy/bar/baz' ), [ 'legacy', 'bar', 'baz' ], 'Legacy prefix, with # and 2 elements' );
+ assert.deepEqual( router.tokenizeHash( 'legacy/bar/baz' ), [ 'legacy', 'bar', 'baz' ], 'Legacy prefix, with 2 elements without #' );
+
+ } );
+}( mediaWiki ) );
diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/routing/mmv.routing.ThumbnailRoute.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/routing/mmv.routing.ThumbnailRoute.test.js
new file mode 100644
index 00000000..0336a628
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/routing/mmv.routing.ThumbnailRoute.test.js
@@ -0,0 +1,32 @@
+/*
+ * This file is part of the MediaWiki extension MediaViewer.
+ *
+ * MediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw ) {
+ QUnit.module( 'mmv.routing.ThumbnailRoute', QUnit.newMwEnvironment() );
+
+ QUnit.test( 'Constructor sanity checks', function ( assert ) {
+ var route,
+ title = new mw.Title( 'File:Foo.png' );
+
+ route = new mw.mmv.routing.ThumbnailRoute( title );
+ assert.ok( route, 'ThumbnailRoute created successfully' );
+
+ assert.ok( mw.mmv.testHelpers.getException( function () {
+ return new mw.mmv.routing.ThumbnailRoute();
+ } ), 'Exception is thrown when ThumbnailRoute is created without arguments' );
+ } );
+}( mediaWiki ) );
diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.canvas.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.canvas.test.js
new file mode 100644
index 00000000..88c74bdb
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.canvas.test.js
@@ -0,0 +1,287 @@
+/*
+ * This file is part of the MediaWiki extension MediaViewer.
+ *
+ * MediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $ ) {
+ QUnit.module( 'mmv.ui.Canvas', QUnit.newMwEnvironment() );
+
+ QUnit.test( 'Constructor sanity check', function ( assert ) {
+ var $qf = $( '#qunit-fixture' ),
+ canvas = new mw.mmv.ui.Canvas( $qf, $qf, $qf );
+
+ assert.ok( canvas.$imageDiv, 'Image container is created.' );
+ assert.strictEqual( canvas.$imageWrapper, $qf, '$imageWrapper is set correctly.' );
+ assert.strictEqual( canvas.$mainWrapper, $qf, '$mainWrapper is set correctly.' );
+ } );
+
+ QUnit.test( 'empty() and set()', function ( assert ) {
+ var $qf = $( '#qunit-fixture' ),
+ canvas = new mw.mmv.ui.Canvas( $qf ),
+ image = new Image(),
+ $imageElem = $( image ),
+ imageRawMetadata = new mw.mmv.LightboxImage( 'foo.png' );
+
+ canvas.empty();
+
+ assert.strictEqual( canvas.$imageDiv.html(), '', 'Canvas is empty.' );
+ assert.ok( canvas.$imageDiv.hasClass( 'empty' ), 'Canvas is not visible.' );
+
+ canvas.set( imageRawMetadata, $imageElem );
+
+ assert.strictEqual( canvas.$image, $imageElem, 'Image element set correctly.' );
+ assert.strictEqual( canvas.imageRawMetadata, imageRawMetadata, 'Raw metadata set correctly.' );
+ assert.strictEqual( canvas.$imageDiv.html(), '<img>', 'Image added to container.' );
+ assert.ok( !canvas.$imageDiv.hasClass( 'empty' ), 'Canvas is visible.' );
+
+ canvas.empty();
+
+ assert.strictEqual( canvas.$imageDiv.html(), '', 'Canvas is empty.' );
+ assert.ok( canvas.$imageDiv.hasClass( 'empty' ), 'Canvas is not visible.' );
+ } );
+
+ QUnit.test( 'setImageAndMaxDimensions()', function ( assert ) {
+ var $qf = $( '#qunit-fixture' ),
+ $mainWrapper = $( '<div>' ).appendTo( $qf ),
+ $innerWrapper = $( '<div>' ).appendTo( $mainWrapper ),
+ $imageWrapper = $( '<div>' ).appendTo( $innerWrapper ),
+ canvas = new mw.mmv.ui.Canvas( $innerWrapper, $imageWrapper, $mainWrapper ),
+ imageRawMetadata = new mw.mmv.LightboxImage( 'foo.png' ),
+ image = new Image(),
+ $imageElem = $( image ),
+ image2 = new Image(),
+ thumbnailWidth = 10,
+ screenWidth = 100,
+ $currentImage,
+ originalWidth;
+
+ // Need to call set() before using setImageAndMaxDimensions()
+ canvas.set( imageRawMetadata, $imageElem );
+ originalWidth = image.width;
+
+ // Call with the same image
+ canvas.setImageAndMaxDimensions(
+ { width: thumbnailWidth },
+ image,
+ { cssWidth: screenWidth }
+ );
+
+ assert.strictEqual( image.width, originalWidth, 'Image width was not modified.' );
+ assert.strictEqual( canvas.$image, $imageElem, 'Image element still set correctly.' );
+
+ $currentImage = canvas.$image;
+
+ // Call with a new image bigger than screen size
+ thumbnailWidth = 150;
+ canvas.setImageAndMaxDimensions(
+ { width: thumbnailWidth },
+ image2,
+ { cssWidth: screenWidth }
+ );
+
+ assert.strictEqual( image2.width, screenWidth, 'Image width was trimmed correctly.' );
+ assert.notStrictEqual( canvas.$image, $currentImage, 'Image element switched correctly.' );
+ } );
+
+ QUnit.test( 'maybeDisplayPlaceholder: Constrained area for SVG files', function ( assert ) {
+ var $image,
+ blurredThumbnailShown,
+ $qf = $( '#qunit-fixture' ),
+ imageRawMetadata = new mw.mmv.LightboxImage( 'foo.svg' ),
+ canvas = new mw.mmv.ui.Canvas( $qf );
+
+ imageRawMetadata.filePageTitle = {
+ getExtension: function () { return 'svg'; }
+ };
+ canvas.imageRawMetadata = imageRawMetadata;
+
+ canvas.set = function () {
+ assert.ok( false, 'Placeholder is not shown' );
+ };
+
+ $image = $( '<img>' ).width( 10 ).height( 5 );
+
+ blurredThumbnailShown = canvas.maybeDisplayPlaceholder(
+ { width: 200, height: 100 },
+ $image,
+ { cssWidth: 300, cssHeight: 150 }
+ );
+
+ assert.strictEqual( $image.width(), 10, 'Placeholder width was not set to max' );
+ assert.strictEqual( $image.height(), 5, 'Placeholder height was not set to max' );
+ assert.ok( !$image.hasClass( 'blurred' ), 'Placeholder is not blurred' );
+ assert.ok( !blurredThumbnailShown, 'Placeholder state is correct' );
+ } );
+
+ QUnit.test( 'maybeDisplayPlaceholder: placeholder big enough that it doesn\'t need blurring, actual image bigger than the lightbox', function ( assert ) {
+ var $image,
+ blurredThumbnailShown,
+ $qf = $( '#qunit-fixture' ),
+ imageRawMetadata = new mw.mmv.LightboxImage( 'foo.png' ),
+ canvas = new mw.mmv.ui.Canvas( $qf );
+
+ imageRawMetadata.filePageTitle = {
+ getExtension: function () { return 'png'; }
+ };
+ canvas.imageRawMetadata = imageRawMetadata;
+
+ canvas.set = function () {
+ assert.ok( true, 'Placeholder shown' );
+ };
+
+ $image = $( '<img>' ).width( 200 ).height( 100 );
+
+ blurredThumbnailShown = canvas.maybeDisplayPlaceholder(
+ { width: 1000, height: 500 },
+ $image,
+ { cssWidth: 300, cssHeight: 150 }
+ );
+
+ assert.strictEqual( $image.width(), 300, 'Placeholder has the right width' );
+ assert.strictEqual( $image.height(), 150, 'Placeholder has the right height' );
+ assert.ok( !$image.hasClass( 'blurred' ), 'Placeholder is not blurred' );
+ assert.ok( !blurredThumbnailShown, 'Placeholder state is correct' );
+ } );
+
+ QUnit.test( 'maybeDisplayPlaceholder: big-enough placeholder that needs blurring, actual image bigger than the lightbox', function ( assert ) {
+ var $image,
+ blurredThumbnailShown,
+ $qf = $( '#qunit-fixture' ),
+ imageRawMetadata = new mw.mmv.LightboxImage( 'foo.png' ),
+ canvas = new mw.mmv.ui.Canvas( $qf );
+
+ imageRawMetadata.filePageTitle = {
+ getExtension: function () { return 'png'; }
+ };
+ canvas.imageRawMetadata = imageRawMetadata;
+
+ canvas.set = function () {
+ assert.ok( true, 'Placeholder shown' );
+ };
+
+ $image = $( '<img>' ).width( 100 ).height( 50 );
+
+ blurredThumbnailShown = canvas.maybeDisplayPlaceholder(
+ { width: 1000, height: 500 },
+ $image,
+ { cssWidth: 300, cssHeight: 150 }
+ );
+
+ assert.strictEqual( $image.width(), 300, 'Placeholder has the right width' );
+ assert.strictEqual( $image.height(), 150, 'Placeholder has the right height' );
+ assert.ok( $image.hasClass( 'blurred' ), 'Placeholder is blurred' );
+ assert.ok( blurredThumbnailShown, 'Placeholder state is correct' );
+ } );
+
+ QUnit.test( 'maybeDisplayPlaceholder: big-enough placeholder that needs blurring, actual image smaller than the lightbox', function ( assert ) {
+ var $image,
+ blurredThumbnailShown,
+ $qf = $( '#qunit-fixture' ),
+ imageRawMetadata = new mw.mmv.LightboxImage( 'foo.png' ),
+ canvas = new mw.mmv.ui.Canvas( $qf );
+
+ imageRawMetadata.filePageTitle = {
+ getExtension: function () { return 'png'; }
+ };
+ canvas.imageRawMetadata = imageRawMetadata;
+
+ canvas.set = function () {
+ assert.ok( true, 'Placeholder shown' );
+ };
+
+ $image = $( '<img>' ).width( 100 ).height( 50 );
+
+ blurredThumbnailShown = canvas.maybeDisplayPlaceholder(
+ { width: 1000, height: 500 },
+ $image,
+ { cssWidth: 1200, cssHeight: 600 }
+ );
+
+ assert.strictEqual( $image.width(), 1000, 'Placeholder has the right width' );
+ assert.strictEqual( $image.height(), 500, 'Placeholder has the right height' );
+ assert.ok( $image.hasClass( 'blurred' ), 'Placeholder is blurred' );
+ assert.ok( blurredThumbnailShown, 'Placeholder state is correct' );
+ } );
+
+ QUnit.test( 'maybeDisplayPlaceholder: placeholder too small to be displayed, actual image bigger than the lightbox', function ( assert ) {
+ var $image,
+ blurredThumbnailShown,
+ $qf = $( '#qunit-fixture' ),
+ imageRawMetadata = new mw.mmv.LightboxImage( 'foo.png' ),
+ canvas = new mw.mmv.ui.Canvas( $qf );
+
+ imageRawMetadata.filePageTitle = {
+ getExtension: function () { return 'png'; }
+ };
+ canvas.imageRawMetadata = imageRawMetadata;
+
+ canvas.set = function () {
+ assert.ok( false, 'Placeholder shown when it should not' );
+ };
+
+ $image = $( '<img>' ).width( 10 ).height( 5 );
+
+ blurredThumbnailShown = canvas.maybeDisplayPlaceholder(
+ { width: 1000, height: 500 },
+ $image,
+ { cssWidth: 300, cssHeight: 150 }
+ );
+
+ assert.strictEqual( $image.width(), 10, 'Placeholder has the right width' );
+ assert.strictEqual( $image.height(), 5, 'Placeholder has the right height' );
+ assert.ok( !$image.hasClass( 'blurred' ), 'Placeholder is not blurred' );
+ assert.ok( !blurredThumbnailShown, 'Placeholder state is correct' );
+ } );
+
+ QUnit.test( 'Unblur', function ( assert ) {
+ var $qf = $( '#qunit-fixture' ),
+ canvas = new mw.mmv.ui.Canvas( $qf ),
+ oldAnimate = $.fn.animate;
+
+ $.fn.animate = function ( target, options ) {
+ var self = this,
+ lastValue;
+
+ $.each( target, function ( key, value ) {
+ lastValue = self.key = value;
+ } );
+
+ if ( options ) {
+ if ( options.step ) {
+ options.step.call( this, lastValue );
+ }
+
+ if ( options.complete ) {
+ options.complete.call( this );
+ }
+ }
+ };
+
+ canvas.$image = $( '<img>' );
+
+ canvas.unblurWithAnimation();
+
+ assert.ok( !canvas.$image.css( '-webkit-filter' ) || !canvas.$image.css( '-webkit-filter' ).length,
+ 'Image has no -webkit-filter left' );
+ assert.ok( !canvas.$image.css( 'filter' ) || !canvas.$image.css( 'filter' ).length || canvas.$image.css( 'filter' ) === 'none',
+ 'Image has no filter left' );
+ assert.strictEqual( parseInt( canvas.$image.css( 'opacity' ), 10 ), 1,
+ 'Image is fully opaque' );
+ assert.ok( !canvas.$image.hasClass( 'blurred' ), 'Image has no "blurred" class' );
+
+ $.fn.animate = oldAnimate;
+ } );
+
+}( mediaWiki, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.canvasButtons.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.canvasButtons.test.js
new file mode 100644
index 00000000..09e5ab9d
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.canvasButtons.test.js
@@ -0,0 +1,36 @@
+/*
+ * This file is part of the MediaWiki extension MediaViewer.
+ *
+ * MediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $ ) {
+ QUnit.module( 'mmv.ui.CanvasButtons', QUnit.newMwEnvironment() );
+
+ QUnit.test( 'Prev/Next', function ( assert ) {
+ var $qf = $( '#qunit-fixture' ),
+ buttons = new mw.mmv.ui.CanvasButtons( $qf, $( '<div>' ), $( '<div>' ) );
+
+ buttons.on( 'next', function () {
+ assert.ok( true, 'Switched to next image' );
+ } );
+
+ buttons.on( 'prev', function () {
+ assert.ok( true, 'Switched to prev image' );
+ } );
+
+ buttons.$next.click();
+ buttons.$prev.click();
+ } );
+}( mediaWiki, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.description.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.description.test.js
new file mode 100644
index 00000000..bcb2b322
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.description.test.js
@@ -0,0 +1,42 @@
+( function ( mw, $ ) {
+ QUnit.module( 'mmv.ui.description', QUnit.newMwEnvironment() );
+
+ QUnit.test( 'Sanity test, object creation and UI construction', function ( assert ) {
+ var description = new mw.mmv.ui.Description( $( '#qunit-fixture' ) );
+
+ assert.ok( description, 'Image description UI element is created' );
+ assert.strictEqual( description.$imageDescDiv.length, 1, 'Image description div is created' );
+ assert.strictEqual( description.$imageDesc.length, 1, 'Image description element is created' );
+ } );
+
+ QUnit.test( 'Setting data in different combinations works well', function ( assert ) {
+ var description = new mw.mmv.ui.Description( $( '#qunit-fixture' ) );
+
+ description.set( null, null );
+ assert.ok( description.$imageDescDiv.hasClass( 'empty' ),
+ 'Image description div is marked empty when neither description nor caption is available' );
+
+ description.set( null, 'foo' );
+ assert.ok( description.$imageDescDiv.hasClass( 'empty' ),
+ 'Image description div is marked empty when there is no description' );
+
+ description.set( 'blah', null );
+ assert.ok( description.$imageDescDiv.hasClass( 'empty' ),
+ 'Image description div is marked empty when there is no caption (description will be shown in title)' );
+
+ description.set( 'foo', 'bar' );
+ assert.ok( !description.$imageDescDiv.hasClass( 'empty' ),
+ 'Image description div is not marked empty when both description and caption are available' );
+ assert.strictEqual( description.$imageDesc.text(), 'foo',
+ 'Image description text is set correctly, caption is ignored' );
+ } );
+
+ QUnit.test( 'Emptying data works as expected', function ( assert ) {
+ var description = new mw.mmv.ui.Description( $( '#qunit-fixture' ) );
+
+ description.set( 'foo', 'bar' );
+ description.empty();
+ assert.strictEqual( description.$imageDescDiv.hasClass( 'empty' ), true, 'Image description div is marked empty when emptied' );
+ assert.strictEqual( description.$imageDesc.text(), '', 'Image description text is emptied correctly' );
+ } );
+}( mediaWiki, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.download.pane.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.download.pane.test.js
new file mode 100644
index 00000000..8cc6008f
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.download.pane.test.js
@@ -0,0 +1,164 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $ ) {
+ QUnit.module( 'mmv.ui.download.pane', QUnit.newMwEnvironment() );
+
+ QUnit.test( 'Sanity test, object creation and UI construction', function ( assert ) {
+ var download = new mw.mmv.ui.download.Pane( $( '#qunit-fixture' ) );
+
+ assert.ok( download, 'download UI element is created.' );
+ assert.strictEqual( download.$pane.length, 1, 'Pane div created.' );
+ assert.ok( download.$downloadButton && download.$selectionArrow, 'Download button created.' );
+ assert.ok( download.downloadSizeMenu, 'Image size pulldown menu created.' );
+ assert.ok( download.$previewLink, 'Preview link created.' );
+ assert.ok( download.defaultItem, 'Default item set.' );
+
+ assert.strictEqual( download.$downloadButton.html(), '', 'Button has empty content.' );
+ assert.strictEqual( download.$downloadButton.attr( 'href' ), undefined, 'Button href is empty.' );
+ assert.strictEqual( download.$previewLink.attr( 'href' ), undefined, 'Preview link href is empty.' );
+ } );
+
+ QUnit.test( 'set()/empty():', function ( assert ) {
+ var download = new mw.mmv.ui.download.Pane( $( '#qunit-fixture' ) ),
+ src = 'https://upload.wikimedia.org/wikipedia/commons/3/3a/Foobar.jpg',
+ image = { // fake mw.mmv.model.Image
+ title: new mw.Title( 'File:Foobar.jpg' ),
+ url: src
+ };
+
+ assert.strictEqual( download.imageExtension, undefined, 'Image extension is not set.' );
+
+ download.utils.updateMenuOptions = function () {
+ assert.ok( true, 'Menu options updated.' );
+ };
+ download.downloadSizeMenu.getMenu().selectItem = function () {
+ assert.ok( true, 'Default item selected to update the labels.' );
+ };
+
+ download.set( image );
+
+ assert.strictEqual( download.imageExtension, 'jpg', 'Image extension is set correctly.' );
+
+ download.empty();
+
+ assert.strictEqual( download.imageExtension, undefined, 'Image extension is not set.' );
+ } );
+
+ QUnit.test( 'attach()/unattach():', function ( assert ) {
+ var hsstub, tstub,
+ download = new mw.mmv.ui.download.Pane( $( '#qunit-fixture' ) ),
+ image = {
+ title: new mw.Title( 'File:Foobar.jpg' ),
+ url: 'https://upload.wikimedia.org/wikipedia/commons/3/3a/Foobar.jpg'
+ };
+
+ download.set( image );
+
+ hsstub = this.sandbox.stub( download, 'handleSizeSwitch' );
+ tstub = this.sandbox.stub( download.downloadSizeMenu.getMenu(), 'toggle' );
+
+ // Triggering action events before attaching should do nothing
+ download.downloadSizeMenu.getMenu().emit(
+ 'choose', download.downloadSizeMenu.getMenu().findSelectedItem() );
+ download.$selectionArrow.click();
+
+ assert.ok( !hsstub.called, 'handleSizeSwitch not called' );
+ assert.ok( !tstub.called, 'Menu selection did not happen' );
+
+ hsstub.reset();
+ tstub.reset();
+
+ download.attach();
+
+ // Action events should be handled now
+ download.downloadSizeMenu.getMenu().emit(
+ 'choose', download.downloadSizeMenu.getMenu().findSelectedItem() );
+ download.$selectionArrow.click();
+
+ assert.ok( hsstub.called, 'handleSizeSwitch was called' );
+ assert.ok( tstub.called, 'Menu selection happened' );
+
+ hsstub.reset();
+ tstub.reset();
+
+ download.unattach();
+
+ // Triggering action events now that we are unattached should do nothing
+ download.downloadSizeMenu.getMenu().emit(
+ 'choose', download.downloadSizeMenu.getMenu().findSelectedItem() );
+ download.$selectionArrow.click();
+
+ assert.ok( !hsstub.called, 'handleSizeSwitch not called' );
+ assert.ok( !tstub.called, 'Menu selection did not happen' );
+ } );
+
+ QUnit.test( 'handleSizeSwitch():', function ( assert ) {
+ var download = new mw.mmv.ui.download.Pane( $( '#qunit-fixture' ) ),
+ newImageUrl = 'https://upload.wikimedia.org/wikipedia/commons/3/3a/NewFoobar.jpg';
+
+ download.utils.getThumbnailUrlPromise = function () {
+ return $.Deferred().resolve( { url: newImageUrl } ).promise();
+ };
+
+ download.setDownloadUrl = function ( url ) {
+ assert.strictEqual( url, newImageUrl, 'URL passed to setDownloadUrl is correct' );
+ };
+
+ download.handleSizeSwitch( download.downloadSizeMenu.getMenu().findSelectedItem() );
+
+ assert.ok( download.$downloadButton.html().match( /original.*/ ), 'Button message updated.' );
+
+ download.image = { url: newImageUrl };
+
+ download.utils.getThumbnailUrlPromise = function () {
+ assert.ok( false, 'Should not fetch the thumbnail if the image is original size.' );
+ };
+
+ download.handleSizeSwitch( download.downloadSizeMenu.getMenu().findSelectedItem() );
+ } );
+
+ QUnit.test( 'setButtonText() sanity check:', function ( assert ) {
+ var download = new mw.mmv.ui.download.Pane( $( '#qunit-fixture' ) ),
+ message;
+
+ download.setButtonText( 'large', 'jpg', 100, 200 );
+ assert.ok( true, 'Setting the text did not cause any errors' );
+
+ message = download.$downloadButton.html();
+ download.setButtonText( 'small', 'png', 1000, 2000 );
+ assert.notStrictEqual( download.$downloadButton.html(), message, 'Button text was updated' );
+ } );
+
+ QUnit.test( 'getExtensionFromUrl():', function ( assert ) {
+ var download = new mw.mmv.ui.download.Pane( $( '#qunit-fixture' ) );
+
+ assert.strictEqual( download.getExtensionFromUrl( 'http://example.com/bing/foo.bar.png' ),
+ 'png', 'Extension is parsed correctly' );
+ } );
+
+ QUnit.test( 'setDownloadUrl', function ( assert ) {
+ var download = new mw.mmv.ui.download.Pane( $( '#qunit-fixture' ) ),
+ imageUrl = 'https://upload.wikimedia.org/wikipedia/commons/3/3a/NewFoobar.jpg';
+
+ download.setDownloadUrl( imageUrl );
+
+ assert.strictEqual( download.$downloadButton.attr( 'href' ), imageUrl + '?download', 'Download link is set correctly.' );
+ assert.strictEqual( download.$previewLink.attr( 'href' ), imageUrl, 'Preview link is set correctly.' );
+ assert.ok( !download.$downloadButton.hasClass( 'disabledLink' ), 'Download link is enabled.' );
+ } );
+}( mediaWiki, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.metadataPanel.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.metadataPanel.test.js
new file mode 100644
index 00000000..c10c6dc9
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.metadataPanel.test.js
@@ -0,0 +1,207 @@
+( function ( mw, $ ) {
+ var thingsShouldBeEmptied = [
+ '$license',
+ '$title',
+ '$location',
+ '$datetime'
+ ],
+
+ thingsShouldHaveEmptyClass = [
+ '$licenseLi',
+ '$credit',
+ '$locationLi',
+ '$datetimeLi'
+ ];
+
+ QUnit.module( 'mmv.ui.metadataPanel', QUnit.newMwEnvironment() );
+
+ QUnit.test( 'The panel is emptied properly when necessary', function ( assert ) {
+ var i,
+ $qf = $( '#qunit-fixture' ),
+ panel = new mw.mmv.ui.MetadataPanel( $qf, $( '<div>' ).appendTo( $qf ), mw.storage, new mw.mmv.Config( {}, mw.config, mw.user, new mw.Api(), mw.storage ) );
+
+ panel.empty();
+
+ assert.expect( thingsShouldBeEmptied.length + thingsShouldHaveEmptyClass.length );
+
+ for ( i = 0; i < thingsShouldBeEmptied.length; i++ ) {
+ assert.strictEqual( panel[ thingsShouldBeEmptied[ i ] ].text(), '', 'We successfully emptied the ' + thingsShouldBeEmptied[ i ] + ' element' );
+ }
+
+ for ( i = 0; i < thingsShouldHaveEmptyClass.length; i++ ) {
+ assert.strictEqual( panel[ thingsShouldHaveEmptyClass[ i ] ].hasClass( 'empty' ), true, 'We successfully applied the empty class to the ' + thingsShouldHaveEmptyClass[ i ] + ' element' );
+ }
+ } );
+
+ QUnit.test( 'Setting location information works as expected', function ( assert ) {
+ var $qf = $( '#qunit-fixture' ),
+ panel = new mw.mmv.ui.MetadataPanel( $qf, $( '<div>' ).appendTo( $qf ), mw.storage, new mw.mmv.Config( {}, mw.config, mw.user, new mw.Api(), mw.storage ) ),
+ fileName = 'Foobar.jpg',
+ latitude = 12.3456789,
+ longitude = 98.7654321,
+ imageData = {
+ latitude: latitude,
+ longitude: longitude,
+ hasCoords: function () { return true; },
+ title: mw.Title.newFromText( 'File:Foobar.jpg' )
+ };
+
+ panel.setLocationData( imageData );
+
+ assert.strictEqual(
+ panel.$location.text(),
+ 'Location: 12° 20′ 44.44″ N, 98° 45′ 55.56″ E',
+ 'Location text is set as expected - if this fails it may be due to i18n issues.'
+ );
+
+ assert.strictEqual(
+ panel.$location.prop( 'href' ),
+ 'http://tools.wmflabs.org/geohack/geohack.php?pagename=File:' + fileName + '&params=' + latitude + '_N_' + longitude + '_E_&language=en',
+ 'Location URL is set as expected'
+ );
+
+ latitude = -latitude;
+ longitude = -longitude;
+ imageData.latitude = latitude;
+ imageData.longitude = longitude;
+ panel.setLocationData( imageData );
+
+ assert.strictEqual(
+ panel.$location.text(),
+ 'Location: 12° 20′ 44.44″ S, 98° 45′ 55.56″ W',
+ 'Location text is set as expected - if this fails it may be due to i18n issues.'
+ );
+
+ assert.strictEqual(
+ panel.$location.prop( 'href' ),
+ 'http://tools.wmflabs.org/geohack/geohack.php?pagename=File:' + fileName + '&params=' + ( -latitude ) + '_S_' + ( -longitude ) + '_W_&language=en',
+ 'Location URL is set as expected'
+ );
+
+ latitude = 0;
+ longitude = 0;
+ imageData.latitude = latitude;
+ imageData.longitude = longitude;
+ panel.setLocationData( imageData );
+
+ assert.strictEqual(
+ panel.$location.text(),
+ 'Location: 0° 0′ 0″ N, 0° 0′ 0″ E',
+ 'Location text is set as expected - if this fails it may be due to i18n issues.'
+ );
+
+ assert.strictEqual(
+ panel.$location.prop( 'href' ),
+ 'http://tools.wmflabs.org/geohack/geohack.php?pagename=File:' + fileName + '&params=' + latitude + '_N_' + longitude + '_E_&language=en',
+ 'Location URL is set as expected'
+ );
+ } );
+
+ QUnit.test( 'Setting image information works as expected', function ( assert ) {
+ var creditPopupText,
+ $qf = $( '#qunit-fixture' ),
+ panel = new mw.mmv.ui.MetadataPanel( $qf, $( '<div>' ).appendTo( $qf ), mw.storage, new mw.mmv.Config( {}, mw.config, mw.user, new mw.Api(), mw.storage ) ),
+ title = 'Foo bar',
+ image = {
+ filePageTitle: mw.Title.newFromText( 'File:' + title + '.jpg' )
+ },
+ imageData = {
+ title: image.filePageTitle,
+ url: 'https://upload.wikimedia.org/wikipedia/commons/3/3a/Foobar.jpg',
+ descriptionUrl: 'https://commons.wikimedia.org/wiki/File:Foobar.jpg',
+ hasCoords: function () { return false; }
+ },
+ repoData = {
+ getArticlePath: function () { return 'Foo'; },
+ isCommons: function () { return false; }
+ },
+ oldMoment = window.moment,
+ // custom clock will give MPP.formatDate some time to load moment.js
+ clock = this.sandbox.useFakeTimers();
+
+ /* window.moment = function ( date ) {
+ // This has no effect for now, since writing this test revealed that our moment.js
+ // doesn't have any language configuration
+ return oldMoment( date ).lang( 'fr' );
+ };*/
+
+ panel.setImageInfo( image, imageData, repoData );
+
+ assert.strictEqual( panel.$title.text(), title, 'Title is correctly set' );
+ assert.ok( panel.$credit.text(), 'Default credit is shown' );
+ assert.strictEqual( panel.$license.prop( 'href' ), imageData.descriptionUrl,
+ 'User is directed to file page for license information' );
+ assert.ok( !panel.$license.prop( 'target' ), 'License information opens in same window' );
+ assert.ok( panel.$datetimeLi.hasClass( 'empty' ), 'Date/Time is empty' );
+ assert.ok( panel.$locationLi.hasClass( 'empty' ), 'Location is empty' );
+
+ imageData.creationDateTime = '2013-08-26T14:41:02Z';
+ imageData.uploadDateTime = '2013-08-25T14:41:02Z';
+ imageData.source = '<b>Lost</b><a href="foo">Bar</a>';
+ imageData.author = 'Bob';
+ imageData.license = new mw.mmv.model.License( 'CC-BY-2.0', 'cc-by-2.0',
+ 'Creative Commons Attribution - Share Alike 2.0',
+ 'http://creativecommons.org/licenses/by-sa/2.0/' );
+ imageData.restrictions = [ 'trademarked', 'default', 'insignia' ];
+
+ panel.setImageInfo( image, imageData, repoData );
+ creditPopupText = panel.creditField.$element.attr( 'original-title' );
+ clock.tick( 10 );
+
+ assert.strictEqual( panel.$title.text(), title, 'Title is correctly set' );
+ assert.ok( !panel.$credit.hasClass( 'empty' ), 'Credit is not empty' );
+ assert.ok( !panel.$datetimeLi.hasClass( 'empty' ), 'Date/Time is not empty' );
+ assert.strictEqual( panel.creditField.$element.find( '.mw-mmv-author' ).text(), imageData.author, 'Author text is correctly set' );
+ assert.strictEqual( panel.creditField.$element.find( '.mw-mmv-source' ).html(), '<b>Lost</b><a href="foo">Bar</a>', 'Source text is correctly set' );
+ // Either multimediaviewer-credit-popup-text or multimediaviewer-credit-popup-text-more.
+ assert.ok( creditPopupText === 'Author and source information' || creditPopupText === 'View full author and source', 'Source tooltip is correctly set' );
+ assert.ok( panel.$datetime.text().indexOf( '26 August 2013' ) > 0, 'Correct date is displayed' );
+ assert.strictEqual( panel.$license.text(), 'CC BY 2.0', 'License is correctly set' );
+ assert.ok( panel.$license.prop( 'target' ), 'License information opens in new window' );
+ assert.ok( panel.$restrictions.children().last().children().hasClass( 'mw-mmv-restriction-default' ), 'Default restriction is correctly displayed last' );
+
+ imageData.creationDateTime = undefined;
+ panel.setImageInfo( image, imageData, repoData );
+ clock.tick( 10 );
+
+ assert.ok( panel.$datetime.text().indexOf( '25 August 2013' ) > 0, 'Correct date is displayed' );
+
+ window.moment = oldMoment;
+ clock.restore();
+ } );
+
+ QUnit.test( 'Setting permission information works as expected', function ( assert ) {
+ var $qf = $( '#qunit-fixture' ),
+ panel = new mw.mmv.ui.MetadataPanel( $qf, $( '<div>' ).appendTo( $qf ), mw.storage, new mw.mmv.Config( {}, mw.config, mw.user, new mw.Api(), mw.storage ) );
+
+ panel.setLicense( null, 'http://example.com' ); // make sure license is visible as it contains the permission
+ panel.setPermission( 'Look at me, I am a permission!' );
+ assert.ok( panel.$permissionLink.is( ':visible' ) );
+ } );
+
+ QUnit.test( 'Date formatting', function ( assert ) {
+ var $qf = $( '#qunit-fixture' ),
+ panel = new mw.mmv.ui.MetadataPanel( $qf, $( '<div>' ).appendTo( $qf ), mw.storage, new mw.mmv.Config( {}, mw.config, mw.user, new mw.Api(), mw.storage ) ),
+ date1 = 'Garbage',
+ promise = panel.formatDate( date1 );
+
+ return promise.then( function ( result ) {
+ assert.strictEqual( result, date1, 'Invalid date is correctly ignored' );
+ } );
+ } );
+
+ QUnit.test( 'About links', function ( assert ) {
+ var $qf = $( '#qunit-fixture' ),
+ oldWgMediaViewerIsInBeta = mw.config.get( 'wgMediaViewerIsInBeta' );
+
+ this.sandbox.stub( mw.user, 'isAnon' );
+ mw.config.set( 'wgMediaViewerIsInBeta', false );
+ // eslint-disable-next-line no-new
+ new mw.mmv.ui.MetadataPanel( $qf.empty(), $( '<div>' ).appendTo( $qf ), mw.storage, new mw.mmv.Config( {}, mw.config, mw.user, new mw.Api(), mw.storage ) );
+
+ assert.strictEqual( $qf.find( '.mw-mmv-about-link' ).length, 1, 'About link is created.' );
+ assert.strictEqual( $qf.find( '.mw-mmv-discuss-link' ).length, 1, 'Discuss link is created.' );
+ assert.strictEqual( $qf.find( '.mw-mmv-help-link' ).length, 1, 'Help link is created.' );
+ mw.config.set( 'wgMediaViewerIsInBeta', oldWgMediaViewerIsInBeta );
+ } );
+}( mediaWiki, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.metadataPanelScroller.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.metadataPanelScroller.test.js
new file mode 100644
index 00000000..7de5aef0
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.metadataPanelScroller.test.js
@@ -0,0 +1,232 @@
+/*
+ * This file is part of the MediaWiki extension MediaViewer.
+ *
+ * MediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $ ) {
+ QUnit.module( 'mmv.ui.metadataPanelScroller', QUnit.newMwEnvironment( {
+ setup: function () {
+ this.clock = this.sandbox.useFakeTimers();
+ }
+ } ) );
+
+ QUnit.test( 'empty()', function ( assert ) {
+ var $qf = $( '#qunit-fixture' ),
+ localStorage = mw.mmv.testHelpers.getFakeLocalStorage(),
+ scroller = new mw.mmv.ui.MetadataPanelScroller( $qf, $( '<div>' ).appendTo( $qf ), localStorage );
+
+ scroller.empty();
+ assert.ok( !scroller.$container.hasClass( 'invite' ), 'We successfully reset the invite' );
+ } );
+
+ QUnit.test( 'Metadata div is only animated once', function ( assert ) {
+ var $qf = $( '#qunit-fixture' ),
+ displayCount = null, // pretend it doesn't exist at first
+ localStorage = mw.mmv.testHelpers.createLocalStorage( {
+ // We simulate localStorage to avoid test side-effects
+ getItem: function () { return displayCount; },
+ setItem: function ( _, val ) { displayCount = val; }
+ } ),
+ scroller = new mw.mmv.ui.MetadataPanelScroller( $qf, $( '<div>' ).appendTo( $qf ), localStorage );
+
+ scroller.attach();
+
+ scroller.animateMetadataOnce();
+
+ assert.ok( scroller.hasAnimatedMetadata,
+ 'The first call to animateMetadataOnce set hasAnimatedMetadata to true' );
+ assert.ok( $qf.hasClass( 'invite' ),
+ 'The first call to animateMetadataOnce led to an animation' );
+
+ $qf.removeClass( 'invite' );
+
+ scroller.animateMetadataOnce();
+
+ assert.strictEqual( scroller.hasAnimatedMetadata, true, 'The second call to animateMetadataOnce did not change the value of hasAnimatedMetadata' );
+ assert.ok( !$qf.hasClass( 'invite' ),
+ 'The second call to animateMetadataOnce did not lead to an animation' );
+
+ scroller.unattach();
+
+ scroller.attach();
+
+ scroller.animateMetadataOnce();
+ assert.ok( $qf.hasClass( 'invite' ),
+ 'After closing and opening the viewer, the panel is animated again' );
+
+ scroller.unattach();
+ } );
+
+ QUnit.test( 'No localStorage', function ( assert ) {
+ var $qf = $( '#qunit-fixture' ),
+ localStorage = mw.mmv.testHelpers.getUnsupportedLocalStorage(),
+ scroller = new mw.mmv.ui.MetadataPanelScroller( $qf, $( '<div>' ).appendTo( $qf ), localStorage );
+
+ this.sandbox.stub( $.fn, 'scrollTop', function () { return 10; } );
+
+ scroller.scroll();
+
+ assert.strictEqual( scroller.hasOpenedMetadata, true, 'We store hasOpenedMetadata flag for the session' );
+ } );
+
+ QUnit.test( 'localStorage is full', function ( assert ) {
+ var $qf = $( '#qunit-fixture' ),
+ localStorage = mw.mmv.testHelpers.createLocalStorage( {
+ getItem: this.sandbox.stub().returns( null ),
+ setItem: this.sandbox.stub().throwsException( 'I am full' )
+ } ),
+ scroller = new mw.mmv.ui.MetadataPanelScroller( $qf, $( '<div>' ).appendTo( $qf ), localStorage );
+
+ this.sandbox.stub( $.fn, 'scrollTop', function () { return 10; } );
+
+ scroller.attach();
+
+ scroller.scroll();
+
+ assert.strictEqual( scroller.hasOpenedMetadata, true, 'We store hasOpenedMetadata flag for the session' );
+
+ scroller.scroll();
+
+ assert.ok( localStorage.store.setItem.calledOnce, 'localStorage only written once' );
+
+ scroller.unattach();
+ } );
+
+ /**
+ * We need to set up a proxy on the jQuery scrollTop function and the jQuery.scrollTo plugin,
+ * that will let us pretend that the document really scrolled and that will return values
+ * as if the scroll happened.
+ *
+ * @param {sinon.sandbox} sandbox
+ * @param {mw.mmv.ui.MetadataPanelScroller} scroller
+ */
+ function stubScrollFunctions( sandbox, scroller ) {
+ var memorizedScrollTop = 0;
+
+ sandbox.stub( $.fn, 'scrollTop', function ( scrollTop ) {
+ if ( scrollTop !== undefined ) {
+ memorizedScrollTop = scrollTop;
+ scroller.scroll();
+ return this;
+ } else {
+ return memorizedScrollTop;
+ }
+ } );
+ sandbox.stub( $.fn, 'animate', function ( props ) {
+ if ( 'scrollTop' in props ) {
+ memorizedScrollTop = props.scrollTop;
+ scroller.scroll();
+ }
+ return this;
+ } );
+ }
+
+ QUnit.test( 'Metadata scrolling', function ( assert ) {
+ var $window = $( window ),
+ $qf = $( '#qunit-fixture' ),
+ $container = $( '<div>' ).css( 'height', 100 ).appendTo( $qf ),
+ $aboveFold = $( '<div>' ).css( 'height', 50 ).appendTo( $container ),
+ fakeLocalStorage = mw.mmv.testHelpers.createLocalStorage( {
+ getItem: this.sandbox.stub().returns( null ),
+ setItem: $.noop
+ } ),
+ scroller = new mw.mmv.ui.MetadataPanelScroller( $container, $aboveFold, fakeLocalStorage ),
+ keydown = $.Event( 'keydown' );
+
+ stubScrollFunctions( this.sandbox, scroller );
+
+ this.sandbox.stub( fakeLocalStorage.store, 'setItem' );
+
+ // First phase of the test: up and down arrows
+
+ scroller.hasAnimatedMetadata = false;
+
+ scroller.attach();
+
+ assert.strictEqual( $window.scrollTop(), 0, 'scrollTop should be set to 0' );
+
+ assert.ok( !fakeLocalStorage.store.setItem.called, 'The metadata hasn\'t been open yet, no entry in localStorage' );
+
+ keydown.which = 38; // Up arrow
+ scroller.keydown( keydown );
+
+ assert.ok( fakeLocalStorage.store.setItem.calledWithExactly( 'mmv.hasOpenedMetadata', '1' ), 'localStorage knows that the metadata has been open' );
+
+ keydown.which = 40; // Down arrow
+ scroller.keydown( keydown );
+
+ assert.strictEqual( $window.scrollTop(), 0,
+ 'scrollTop should be set to 0 after pressing down arrow' );
+
+ // Unattach lightbox from document
+ scroller.unattach();
+
+ // Second phase of the test: scroll memory
+
+ scroller.attach();
+
+ // To make sure that the details are out of view, the lightbox is supposed to scroll to the top when open
+ assert.strictEqual( $window.scrollTop(), 0, 'Page scrollTop should be set to 0' );
+
+ // Scroll down to check that the scrollTop memory doesn't affect prev/next (bug 59861)
+ $window.scrollTop( 20 );
+ this.clock.tick( 100 );
+
+ // This extra attach() call simulates the effect of prev/next seen in bug 59861
+ scroller.attach();
+
+ // The lightbox was already open at this point, the scrollTop should be left untouched
+ assert.strictEqual( $window.scrollTop(), 20, 'Page scrollTop should be set to 20' );
+
+ scroller.unattach();
+ } );
+
+ QUnit.test( 'Metadata scroll logging', function ( assert ) {
+ var $qf = $( '#qunit-fixture' ),
+ $container = $( '<div>' ).css( 'height', 100 ).appendTo( $qf ),
+ $aboveFold = $( '<div>' ).css( 'height', 50 ).appendTo( $container ),
+ localStorage = mw.mmv.testHelpers.getFakeLocalStorage(),
+ scroller = new mw.mmv.ui.MetadataPanelScroller( $container, $aboveFold, localStorage ),
+ keydown = $.Event( 'keydown' );
+
+ stubScrollFunctions( this.sandbox, scroller );
+
+ this.sandbox.stub( mw.mmv.actionLogger, 'log' );
+
+ keydown.which = 38; // Up arrow
+ scroller.keydown( keydown );
+
+ assert.ok( mw.mmv.actionLogger.log.calledWithExactly( 'metadata-open' ), 'Opening keypress logged' );
+ mw.mmv.actionLogger.log.reset();
+
+ keydown.which = 38; // Up arrow
+ scroller.keydown( keydown );
+
+ assert.ok( mw.mmv.actionLogger.log.calledWithExactly( 'metadata-close' ), 'Closing keypress logged' );
+ mw.mmv.actionLogger.log.reset();
+
+ keydown.which = 40; // Down arrow
+ scroller.keydown( keydown );
+
+ assert.ok( mw.mmv.actionLogger.log.calledWithExactly( 'metadata-open' ), 'Opening keypress logged' );
+ mw.mmv.actionLogger.log.reset();
+
+ keydown.which = 40; // Down arrow
+ scroller.keydown( keydown );
+
+ assert.ok( mw.mmv.actionLogger.log.calledWithExactly( 'metadata-close' ), 'Closing keypress logged' );
+ mw.mmv.actionLogger.log.reset();
+ } );
+}( mediaWiki, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.permission.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.permission.test.js
new file mode 100644
index 00000000..319642e3
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.permission.test.js
@@ -0,0 +1,112 @@
+/*
+ * This file is part of the MediaWiki extension MediaViewer.
+ *
+ * MediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $ ) {
+ QUnit.module( 'mw.mmv.ui.Permission', QUnit.newMwEnvironment( {
+ setup: function () {
+ // animation would keep running, conflict with other tests
+ this.sandbox.stub( $.fn, 'animate' ).returnsThis();
+ }
+ } ) );
+
+ QUnit.test( 'Constructor sanity check', function ( assert ) {
+ var $qf = $( '#qunit-fixture' ),
+ permission = new mw.mmv.ui.Permission( $qf );
+
+ assert.ok( permission, 'constructor does not throw error' );
+ } );
+
+ QUnit.test( 'set()', function ( assert ) {
+ var $qf = $( '#qunit-fixture' ),
+ permission = new mw.mmv.ui.Permission( $qf ),
+ text = 'Nothing to see here.';
+
+ permission.set( text );
+
+ // FIXME get rid of "view more" - this is temporary
+ assert.strictEqual( permission.$text.children().remove().end().text(),
+ text, 'permission text is set' );
+ assert.strictEqual( permission.$html.text(), text, 'permission html is set' );
+ assert.ok( permission.$text.is( ':visible' ), 'permission text is visible' );
+ assert.ok( !permission.$html.is( ':visible' ), 'permission html is not visible' );
+ assert.ok( !permission.$close.is( ':visible' ), 'close button is not visible' );
+ } );
+
+ QUnit.test( 'set() with html', function ( assert ) {
+ var $qf = $( '#qunit-fixture' ),
+ permission = new mw.mmv.ui.Permission( $qf ),
+ text = '<b>Nothing</b> to see here.';
+
+ permission.set( text );
+
+ assert.ok( !permission.$text.find( 'b' ).length, 'permission text has no html' );
+ assert.ok( permission.$html.find( 'b' ), 'permission html has html' );
+ } );
+
+ QUnit.test( 'empty()', function ( assert ) {
+ var $qf = $( '#qunit-fixture' ),
+ permission = new mw.mmv.ui.Permission( $qf ),
+ text = 'Nothing to see here.';
+
+ permission.set( text );
+ permission.empty();
+
+ assert.ok( !permission.$text.is( ':visible' ), 'permission text is not visible' );
+ assert.ok( !permission.$html.is( ':visible' ), 'permission html is not visible' );
+ assert.ok( !permission.$close.is( ':visible' ), 'close button is not visible' );
+ } );
+
+ QUnit.test( 'grow()', function ( assert ) {
+ var $qf = $( '#qunit-fixture' ),
+ permission = new mw.mmv.ui.Permission( $qf ),
+ text = 'Nothing to see here.';
+
+ permission.set( text );
+ permission.grow();
+
+ assert.ok( !permission.$text.is( ':visible' ), 'permission text is not visible' );
+ assert.ok( permission.$html.is( ':visible' ), 'permission html is visible' );
+ assert.ok( permission.$close.is( ':visible' ), 'close button is visible' );
+ } );
+
+ QUnit.test( 'shrink()', function ( assert ) {
+ var $qf = $( '#qunit-fixture' ),
+ permission = new mw.mmv.ui.Permission( $qf ),
+ text = 'Nothing to see here.';
+
+ permission.set( text );
+ permission.grow();
+ permission.shrink();
+
+ assert.ok( permission.$text.is( ':visible' ), 'permission text is visible' );
+ assert.ok( !permission.$html.is( ':visible' ), 'permission html is not visible' );
+ assert.ok( !permission.$close.is( ':visible' ), 'close button is not visible' );
+ } );
+
+ QUnit.test( 'isFullSize()', function ( assert ) {
+ var $qf = $( '#qunit-fixture' ),
+ permission = new mw.mmv.ui.Permission( $qf ),
+ text = 'Nothing to see here.';
+
+ permission.set( text );
+ assert.ok( !permission.isFullSize(), 'permission is not full-size' );
+ permission.grow();
+ assert.ok( permission.isFullSize(), 'permission is full-size' );
+ permission.shrink();
+ assert.ok( !permission.isFullSize(), 'permission is not full-size again' );
+ } );
+}( mediaWiki, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.progressBar.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.progressBar.test.js
new file mode 100644
index 00000000..5b3bd3d0
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.progressBar.test.js
@@ -0,0 +1,77 @@
+/*
+ * This file is part of the MediaWiki extension MediaViewer.
+ *
+ * MediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $ ) {
+ QUnit.module( 'mmv.ui.ProgressBar', QUnit.newMwEnvironment() );
+
+ QUnit.test( 'Constructor sanity check', function ( assert ) {
+ var progressBar = new mw.mmv.ui.ProgressBar( $( '<div>' ) );
+ assert.ok( progressBar, 'ProgressBar created sccessfully' );
+ assert.ok( progressBar.$progress.hasClass( 'empty' ), 'ProgressBar starts empty' );
+ } );
+
+ QUnit.test( 'animateTo()', function ( assert ) {
+ var $qf = $( '#qunit-fixture' ),
+ $div = $( '<div>' ).css( { width: 250, position: 'relative' } ).appendTo( $qf ),
+ progress = new mw.mmv.ui.ProgressBar( $div );
+
+ assert.ok( progress.$progress.hasClass( 'empty' ), 'Progress bar is hidden' );
+ assert.strictEqual( progress.$percent.width(), 0, 'Progress bar\'s indicator is at 0' );
+
+ this.sandbox.stub( $.fn, 'animate', function ( target ) {
+ $( this ).css( target );
+ assert.strictEqual( target.width, '50%', 'Animation should go to 50%' );
+ } );
+ progress.animateTo( 50 );
+ assert.ok( !progress.$progress.hasClass( 'empty' ), 'Progress bar is visible' );
+
+ assert.strictEqual( progress.$percent.width(), 125, 'Progress bar\'s indicator is at half' );
+
+ $.fn.animate.restore();
+ this.sandbox.stub( $.fn, 'animate', function ( target, duration, transition, callback ) {
+ $( this ).css( target );
+
+ assert.strictEqual( target.width, '100%', 'Animation should go to 100%' );
+
+ if ( callback !== undefined ) {
+ callback();
+ }
+ } );
+ progress.animateTo( 100 );
+ assert.ok( progress.$progress.hasClass( 'empty' ), 'Progress bar is hidden' );
+ assert.strictEqual( progress.$percent.width(), 0, 'Progress bar\'s indicator is at 0' );
+ } );
+
+ QUnit.test( 'jumpTo()/hide()', function ( assert ) {
+ var $qf = $( '#qunit-fixture' ),
+ $div = $( '<div>' ).css( { width: 250, position: 'relative' } ).appendTo( $qf ),
+ progress = new mw.mmv.ui.ProgressBar( $div );
+
+ assert.ok( progress.$progress.hasClass( 'empty' ), 'Progress bar is hidden' );
+ assert.strictEqual( progress.$percent.width(), 0, 'Progress bar\'s indicator is at 0' );
+
+ progress.jumpTo( 50 );
+
+ assert.ok( !progress.$progress.hasClass( 'empty' ), 'Progress bar is visible' );
+ assert.strictEqual( progress.$percent.width(), 125, 'Progress bar\'s indicator is at half' );
+
+ progress.hide();
+
+ assert.ok( progress.$progress.hasClass( 'empty' ), 'Progress bar is hidden' );
+ assert.strictEqual( progress.$percent.width(), 0, 'Progress bar\'s indicator is at 0' );
+ } );
+}( mediaWiki, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.reuse.dialog.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.reuse.dialog.test.js
new file mode 100644
index 00000000..01322125
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.reuse.dialog.test.js
@@ -0,0 +1,250 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $ ) {
+ function makeReuseDialog( sandbox ) {
+ var $fixture = $( '#qunit-fixture' ),
+ config = { getFromLocalStorage: sandbox.stub(), setInLocalStorage: sandbox.stub() };
+ return new mw.mmv.ui.reuse.Dialog( $fixture, $( '<div>' ).appendTo( $fixture ), config );
+ }
+
+ QUnit.module( 'mmv.ui.reuse.Dialog', QUnit.newMwEnvironment() );
+
+ QUnit.test( 'Sanity test, object creation and UI construction', function ( assert ) {
+ var reuseDialog = makeReuseDialog( this.sandbox );
+
+ assert.ok( reuseDialog, 'Reuse UI element is created.' );
+ assert.strictEqual( reuseDialog.$dialog.length, 1, 'Reuse dialog div created.' );
+ } );
+
+ QUnit.test( 'handleOpenCloseClick():', function ( assert ) {
+ var reuseDialog = makeReuseDialog( this.sandbox );
+
+ reuseDialog.openDialog = function () {
+ assert.ok( true, 'openDialog called.' );
+ };
+ reuseDialog.closeDialog = function () {
+ assert.ok( false, 'closeDialog should not have been called.' );
+ };
+
+ // Dialog is closed by default, we should open it
+ reuseDialog.handleOpenCloseClick();
+
+ reuseDialog.openDialog = function () {
+ assert.ok( false, 'openDialog should not have been called.' );
+ };
+ reuseDialog.closeDialog = function () {
+ assert.ok( true, 'closeDialog called.' );
+ };
+ reuseDialog.isOpen = true;
+
+ // Dialog open now, we should close it.
+ reuseDialog.handleOpenCloseClick();
+ } );
+
+ QUnit.test( 'handleTabSelection():', function ( assert ) {
+ var reuseDialog = makeReuseDialog( this.sandbox );
+
+ reuseDialog.initTabs();
+
+ // Share pane is selected
+ reuseDialog.handleTabSelection( { getData: function () { return 'share'; } } );
+ assert.ok( reuseDialog.tabs.share.$pane.hasClass( 'active' ), 'Share tab shown.' );
+ assert.ok( !reuseDialog.tabs.embed.$pane.hasClass( 'active' ), 'Embed tab hidden.' );
+ assert.ok( reuseDialog.config.setInLocalStorage.calledWith( 'mmv-lastUsedTab', 'share' ),
+ 'Tab state saved in local storage.' );
+
+ // Embed pane is selected
+ reuseDialog.handleTabSelection( { getData: function () { return 'embed'; } } );
+ assert.ok( !reuseDialog.tabs.share.$pane.hasClass( 'active' ), 'Share tab hidden.' );
+ assert.ok( reuseDialog.tabs.embed.$pane.hasClass( 'active' ), 'Embed tab shown.' );
+ } );
+
+ QUnit.test( 'default tab:', function ( assert ) {
+ var reuseDialog;
+
+ reuseDialog = makeReuseDialog( this.sandbox );
+ reuseDialog.initTabs();
+ assert.strictEqual( reuseDialog.selectedTab, 'share', 'Share tab is default' );
+
+ reuseDialog = makeReuseDialog( this.sandbox );
+ reuseDialog.config.getFromLocalStorage.withArgs( 'mmv-lastUsedTab' ).returns( 'share' );
+ reuseDialog.initTabs();
+ assert.strictEqual( reuseDialog.selectedTab, 'share', 'Default can be overridden' );
+ } );
+
+ QUnit.test( 'attach()/unattach():', function ( assert ) {
+ var reuseDialog = makeReuseDialog( this.sandbox );
+
+ reuseDialog.initTabs();
+
+ reuseDialog.handleOpenCloseClick = function () {
+ assert.ok( false, 'handleOpenCloseClick should not have been called.' );
+ };
+ reuseDialog.handleTabSelection = function () {
+ assert.ok( false, 'handleTabSelection should not have been called.' );
+ };
+
+ // Triggering action events before attaching should do nothing
+ $( document ).trigger( 'mmv-reuse-open' );
+ reuseDialog.reuseTabs.emit( 'select' );
+
+ reuseDialog.handleOpenCloseClick = function () {
+ assert.ok( true, 'handleOpenCloseClick called.' );
+ };
+ reuseDialog.handleTabSelection = function () {
+ assert.ok( true, 'handleTabSelection called.' );
+ };
+
+ reuseDialog.attach();
+
+ // Action events should be handled now
+ $( document ).trigger( 'mmv-reuse-open' );
+ reuseDialog.reuseTabs.emit( 'select' );
+
+ // Test the unattach part
+ reuseDialog.handleOpenCloseClick = function () {
+ assert.ok( false, 'handleOpenCloseClick should not have been called.' );
+ };
+ reuseDialog.handleTabSelection = function () {
+ assert.ok( false, 'handleTabSelection should not have been called.' );
+ };
+
+ reuseDialog.unattach();
+
+ // Triggering action events now that we are unattached should do nothing
+ $( document ).trigger( 'mmv-reuse-open' );
+ reuseDialog.reuseTabs.emit( 'select' );
+ } );
+
+ QUnit.test( 'start/stopListeningToOutsideClick():', function ( assert ) {
+ var reuseDialog = makeReuseDialog( this.sandbox ),
+ realCloseDialog = reuseDialog.closeDialog;
+
+ reuseDialog.initTabs();
+
+ function clickOutsideDialog() {
+ var event = new $.Event( 'click', { target: reuseDialog.$container[ 0 ] } );
+ reuseDialog.$container.trigger( event );
+ return event;
+ }
+ function clickInsideDialog() {
+ var event = new $.Event( 'click', { target: reuseDialog.$dialog[ 0 ] } );
+ reuseDialog.$dialog.trigger( event );
+ return event;
+ }
+
+ function assertDialogDoesNotCatchClicks() {
+ var event;
+ reuseDialog.closeDialog = function () { assert.ok( false, 'Dialog is not affected by click' ); };
+ event = clickOutsideDialog();
+ assert.ok( !event.isDefaultPrevented(), 'Dialog does not affect click' );
+ assert.ok( !event.isPropagationStopped(), 'Dialog does not affect click propagation' );
+ }
+ function assertDialogCatchesOutsideClicksOnly() {
+ var event;
+ reuseDialog.closeDialog = function () { assert.ok( false, 'Dialog is not affected by inside click' ); };
+ event = clickInsideDialog();
+ assert.ok( !event.isDefaultPrevented(), 'Dialog does not affect inside click' );
+ assert.ok( !event.isPropagationStopped(), 'Dialog does not affect inside click propagation' );
+ reuseDialog.closeDialog = function () { assert.ok( true, 'Dialog is closed by outside click' ); };
+ event = clickOutsideDialog();
+ assert.ok( event.isDefaultPrevented(), 'Dialog catches outside click' );
+ assert.ok( event.isPropagationStopped(), 'Dialog stops outside click propagation' );
+ }
+
+ assertDialogDoesNotCatchClicks();
+ reuseDialog.openDialog();
+ assertDialogCatchesOutsideClicksOnly();
+ realCloseDialog.call( reuseDialog );
+ assertDialogDoesNotCatchClicks();
+ reuseDialog.openDialog();
+ reuseDialog.unattach();
+ assertDialogDoesNotCatchClicks();
+ } );
+
+ QUnit.test( 'set()/empty() sanity check:', function ( assert ) {
+ var reuseDialog = makeReuseDialog( this.sandbox ),
+ title = mw.Title.newFromText( 'File:Foobar.jpg' ),
+ src = 'https://upload.wikimedia.org/wikipedia/commons/3/3a/Foobar.jpg',
+ url = 'https://commons.wikimedia.org/wiki/File:Foobar.jpg',
+ image = { // fake mw.mmv.model.Image
+ title: title,
+ url: src,
+ descriptionUrl: url,
+ width: 100,
+ height: 80
+ },
+ embedFileInfo = new mw.mmv.model.EmbedFileInfo( title, src, url );
+
+ reuseDialog.set( image, embedFileInfo );
+ reuseDialog.empty();
+
+ assert.ok( true, 'Set/empty did not cause an error.' );
+ } );
+
+ QUnit.test( 'openDialog()/closeDialog():', function ( assert ) {
+ var reuseDialog = makeReuseDialog( this.sandbox ),
+ title = mw.Title.newFromText( 'File:Foobar.jpg' ),
+ src = 'https://upload.wikimedia.org/wikipedia/commons/3/3a/Foobar.jpg',
+ url = 'https://commons.wikimedia.org/wiki/File:Foobar.jpg',
+ image = { // fake mw.mmv.model.Image
+ title: title,
+ url: src,
+ descriptionUrl: url,
+ width: 100,
+ height: 80
+ },
+ repoInfo = new mw.mmv.model.Repo( 'Wikipedia', '//wikipedia.org/favicon.ico', true );
+
+ reuseDialog.initTabs();
+
+ reuseDialog.set( image, repoInfo );
+
+ assert.ok( !reuseDialog.isOpen, 'Dialog closed by default.' );
+
+ reuseDialog.openDialog();
+
+ assert.ok( reuseDialog.isOpen, 'Dialog open now.' );
+
+ reuseDialog.closeDialog();
+
+ assert.ok( !reuseDialog.isOpen, 'Dialog closed now.' );
+ } );
+
+ QUnit.test( 'getImageWarnings():', function ( assert ) {
+ var reuseDialog = makeReuseDialog( this.sandbox ),
+ title = mw.Title.newFromText( 'File:Foobar.jpg' ),
+ src = 'https://upload.wikimedia.org/wikipedia/commons/3/3a/Foobar.jpg',
+ url = 'https://commons.wikimedia.org/wiki/File:Foobar.jpg',
+ image = { // fake mw.mmv.model.Image
+ title: title,
+ url: src,
+ descriptionUrl: url,
+ width: 100,
+ height: 80
+ },
+ imageDeleted = $.extend( { deletionReason: 'deleted file test' }, image );
+
+ // Test that the lack of license is picked up
+ assert.equal( 1, reuseDialog.getImageWarnings( image ).length, 'Lack of license detected' );
+
+ // Test that deletion supersedes other warnings and only that one is reported
+ assert.equal( 1, reuseDialog.getImageWarnings( imageDeleted ).length, 'Deletion detected' );
+ } );
+
+}( mediaWiki, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.reuse.embed.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.reuse.embed.test.js
new file mode 100644
index 00000000..69ca2466
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.reuse.embed.test.js
@@ -0,0 +1,398 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $ ) {
+ var $qf = $( '#qunit-fixture' );
+
+ QUnit.module( 'mmv.ui.reuse.Embed', QUnit.newMwEnvironment() );
+
+ QUnit.test( 'Sanity test, object creation and UI construction', function ( assert ) {
+ var embed = new mw.mmv.ui.reuse.Embed( $qf );
+
+ assert.ok( embed, 'Embed UI element is created.' );
+ assert.strictEqual( embed.$pane.length, 1, 'Pane div is created.' );
+ assert.ok( embed.embedTextHtml, 'Html snipped text area created.' );
+ assert.ok( embed.embedTextWikitext, 'Wikitext snipped text area created.' );
+ assert.ok( embed.embedSwitch, 'Snipped selection buttons created.' );
+ assert.ok( embed.embedSizeSwitchWikitext, 'Size selection menu for wikitext created.' );
+ assert.ok( embed.embedSizeSwitchHtml, 'Size selection menu for html created.' );
+ assert.strictEqual( embed.$currentMainEmbedText.length, 1, 'Size selection menu for html created.' );
+ assert.strictEqual( embed.isSizeMenuDefaultReset, false, 'Reset flag intialized correctly.' );
+ assert.ok( embed.defaultHtmlItem, 'Default item for html size selection intialized.' );
+ assert.ok( embed.defaultWikitextItem, 'Default item for wikitext size selection intialized.' );
+ assert.ok( embed.currentSizeMenu, 'Current size menu intialized.' );
+ assert.ok( embed.currentDefaultItem, 'Current default item intialized.' );
+ } );
+
+ QUnit.test( 'changeSize(): Skip if no item selected.', function ( assert ) {
+ var embed = new mw.mmv.ui.reuse.Embed( $qf ),
+ width = 10,
+ height = 20;
+
+ assert.expect( 0 );
+
+ // deselect items
+ embed.embedSwitch.selectItem();
+
+ embed.updateEmbedHtml = function () {
+ assert.ok( false, 'No item selected, this should not have been called.' );
+ };
+ embed.updateEmbedWikitext = function () {
+ assert.ok( false, 'No item selected, this should not have been called.' );
+ };
+
+ embed.changeSize( width, height );
+ } );
+
+ QUnit.test( 'changeSize(): HTML size menu item selected.', function ( assert ) {
+ var embed = new mw.mmv.ui.reuse.Embed( $qf ),
+ width = 10,
+ height = 20;
+
+ embed.embedSwitch.findSelectedItem = function () {
+ return { getData: function () { return 'html'; } };
+ };
+ embed.updateEmbedHtml = function ( thumb, w, h ) {
+ assert.strictEqual( thumb.url, undefined, 'Empty thumbnail passed.' );
+ assert.strictEqual( w, width, 'Correct width passed.' );
+ assert.strictEqual( h, height, 'Correct height passed.' );
+ };
+ embed.updateEmbedWikitext = function () {
+ assert.ok( false, 'Dealing with HTML menu, this should not have been called.' );
+ };
+ embed.select = function () {
+ assert.ok( true, 'Item selected after update.' );
+ };
+
+ embed.changeSize( width, height );
+ } );
+
+ QUnit.test( 'changeSize(): Wikitext size menu item selected.', function ( assert ) {
+ var embed = new mw.mmv.ui.reuse.Embed( $qf ),
+ width = 10,
+ height = 20;
+
+ embed.embedSwitch.findSelectedItem = function () {
+ return { getData: function () { return 'wikitext'; } };
+ };
+ embed.updateEmbedHtml = function () {
+ assert.ok( false, 'Dealing with wikitext menu, this should not have been called.' );
+ };
+ embed.updateEmbedWikitext = function ( w ) {
+ assert.strictEqual( w, width, 'Correct width passed.' );
+ };
+ embed.select = function () {
+ assert.ok( true, 'Item selected after update.' );
+ };
+
+ embed.changeSize( width, height );
+ } );
+
+ QUnit.test( 'updateEmbedHtml(): Do nothing if set() not called before.', function ( assert ) {
+ var embed = new mw.mmv.ui.reuse.Embed( $qf ),
+ width = 10,
+ height = 20;
+
+ assert.expect( 0 );
+
+ embed.formatter.getThumbnailHtml = function () {
+ assert.ok( false, 'formatter.getThumbnailHtml() should not have been called.' );
+ };
+ embed.updateEmbedHtml( {}, width, height );
+ } );
+
+ QUnit.test( 'updateEmbedHtml():', function ( assert ) {
+ var embed = new mw.mmv.ui.reuse.Embed( $qf ),
+ url = 'https://upload.wikimedia.org/wikipedia/commons/3/3a/Foobar.jpg',
+ thumbUrl = 'https://upload.wikimedia.org/wikipedia/thumb/Foobar.jpg',
+ imageInfo = { url: url },
+ repoInfo = {},
+ caption = '-',
+ info = new mw.mmv.model.EmbedFileInfo( imageInfo, repoInfo, caption ),
+ width = 10,
+ height = 20;
+
+ embed.set( imageInfo, repoInfo, caption );
+
+ // Small image, no thumbnail info is passed
+ embed.formatter.getThumbnailHtml = function ( i, u, w, h ) {
+ assert.deepEqual( i, info, 'Info passed correctly.' );
+ assert.strictEqual( u, url, 'Image URL passed correctly.' );
+ assert.strictEqual( w, width, 'Correct width passed.' );
+ assert.strictEqual( h, height, 'Correct height passed.' );
+ };
+ embed.updateEmbedHtml( {}, width, height );
+
+ // Small image, thumbnail info present
+ embed.formatter.getThumbnailHtml = function ( i, u ) {
+ assert.strictEqual( u, thumbUrl, 'Image src passed correctly.' );
+ };
+ embed.updateEmbedHtml( { url: thumbUrl }, width, height );
+
+ // Big image, thumbnail info present
+ embed.formatter.getThumbnailHtml = function ( i, u ) {
+ assert.strictEqual( u, url, 'Image src passed correctly.' );
+ };
+ width = 1300;
+ embed.updateEmbedHtml( { url: thumbUrl }, width, height );
+ } );
+
+ QUnit.test( 'updateEmbedWikitext(): Do nothing if set() not called before.', function ( assert ) {
+ var embed = new mw.mmv.ui.reuse.Embed( $qf ),
+ width = 10;
+
+ assert.expect( 0 );
+
+ embed.formatter.getThumbnailWikitext = function () {
+ assert.ok( false, 'formatter.getThumbnailWikitext() should not have been called.' );
+ };
+ embed.updateEmbedWikitext( width );
+ } );
+
+ QUnit.test( 'updateEmbedWikitext():', function ( assert ) {
+ var embed = new mw.mmv.ui.reuse.Embed( $qf ),
+ imageInfo = {},
+ repoInfo = {},
+ caption = '-',
+ info = new mw.mmv.model.EmbedFileInfo( imageInfo, repoInfo, caption ),
+ width = 10;
+
+ embed.set( imageInfo, repoInfo, caption );
+
+ embed.formatter.getThumbnailWikitextFromEmbedFileInfo = function ( i, w ) {
+ assert.deepEqual( i, info, 'EmbedFileInfo passed correctly.' );
+ assert.strictEqual( w, width, 'Width passed correctly.' );
+ };
+ embed.updateEmbedWikitext( width );
+ } );
+
+ QUnit.test( 'getPossibleImageSizesForWikitext()', function ( assert ) {
+ var embed = new mw.mmv.ui.reuse.Embed( $qf ),
+ exampleSizes = [
+ // Big wide image
+ {
+ width: 2048, height: 1536,
+ expected: {
+ small: { width: 300, height: 225 },
+ medium: { width: 400, height: 300 },
+ large: { width: 500, height: 375 },
+ 'default': { width: null, height: null }
+ }
+ },
+
+ // Big tall image
+ {
+ width: 201, height: 1536,
+ expected: {
+ 'default': { width: null, height: null }
+ }
+ },
+
+ // Very small image
+ {
+ width: 15, height: 20,
+ expected: {
+ 'default': { width: null, height: null }
+ }
+ }
+ ],
+ i, cursize, opts;
+ for ( i = 0; i < exampleSizes.length; i++ ) {
+ cursize = exampleSizes[ i ];
+ opts = embed.getPossibleImageSizesForWikitext( cursize.width, cursize.height );
+ assert.deepEqual( opts, cursize.expected, 'We got the expected results out of the size calculation function.' );
+ }
+ } );
+
+ QUnit.test( 'set():', function ( assert ) {
+ var embed = new mw.mmv.ui.reuse.Embed( $qf ),
+ title = mw.Title.newFromText( 'File:Foobar.jpg' ),
+ src = 'https://upload.wikimedia.org/wikipedia/commons/3/3a/Foobar.jpg',
+ url = 'https://commons.wikimedia.org/wiki/File:Foobar.jpg',
+ embedFileInfo = new mw.mmv.model.EmbedFileInfo( title, src, url ),
+ calledSelect = false,
+ width = 15,
+ height = 20;
+
+ embed.utils.updateMenuOptions = function ( sizes, options ) {
+ assert.strictEqual( options.length, 4, 'Options passed correctly.' );
+ };
+ embed.resetCurrentSizeMenuToDefault = function () {
+ assert.ok( true, 'resetCurrentSizeMenuToDefault() is called.' );
+ };
+ embed.utils.getThumbnailUrlPromise = function () {
+ return $.Deferred().resolve().promise();
+ };
+ embed.updateEmbedHtml = function () {
+ assert.ok( true, 'updateEmbedHtml() is called after data is collected.' );
+ };
+ embed.select = function () {
+ calledSelect = true;
+ };
+
+ assert.ok( !embed.embedFileInfo, 'embedFileInfo not set yet.' );
+
+ embed.set( { width: width, height: height }, embedFileInfo );
+
+ assert.ok( embed.embedFileInfo, 'embedFileInfo correctly set.' );
+ assert.strictEqual( embed.isSizeMenuDefaultReset, false, 'Reset flag cleared.' );
+ assert.strictEqual( calledSelect, true, 'select() is called' );
+ } );
+
+ QUnit.test( 'empty():', function ( assert ) {
+ var embed = new mw.mmv.ui.reuse.Embed( $qf ),
+ width = 15,
+ height = 20;
+
+ embed.formatter = {
+ getThumbnailWikitextFromEmbedFileInfo: function () { return 'wikitext'; },
+ getThumbnailHtml: function () { return 'html'; }
+ };
+
+ embed.set( {}, {} );
+ embed.updateEmbedHtml( { url: 'x' }, width, height );
+ embed.updateEmbedWikitext( width );
+
+ assert.notStrictEqual( embed.embedTextHtml.getValue(), '', 'embedTextHtml is not empty.' );
+ assert.notStrictEqual( embed.embedTextWikitext.getValue(), '', 'embedTextWikitext is not empty.' );
+
+ embed.empty();
+
+ assert.strictEqual( embed.embedTextHtml.getValue(), '', 'embedTextHtml is empty.' );
+ assert.strictEqual( embed.embedTextWikitext.getValue(), '', 'embedTextWikitext is empty.' );
+ assert.ok( !embed.embedSizeSwitchHtml.getMenu().isVisible(), 'Html size menu should be hidden.' );
+ assert.ok( !embed.embedSizeSwitchWikitext.getMenu().isVisible(), 'Wikitext size menu should be hidden.' );
+ } );
+
+ QUnit.test( 'attach()/unattach():', function ( assert ) {
+ var embed = new mw.mmv.ui.reuse.Embed( $qf ),
+ title = mw.Title.newFromText( 'File:Foobar.jpg' ),
+ src = 'https://upload.wikimedia.org/wikipedia/commons/3/3a/Foobar.jpg',
+ url = 'https://commons.wikimedia.org/wiki/File:Foobar.jpg',
+ embedFileInfo = new mw.mmv.model.EmbedFileInfo( title, src, url ),
+ width = 15,
+ height = 20;
+
+ embed.set( { width: width, height: height }, embedFileInfo );
+
+ embed.selectAllOnEvent = function () {
+ assert.ok( false, 'selectAllOnEvent should not have been called.' );
+ };
+ embed.handleTypeSwitch = function () {
+ assert.ok( false, 'handleTypeSwitch should not have been called.' );
+ };
+ embed.handleSizeSwitch = function () {
+ assert.ok( false, 'handleTypeSwitch should not have been called.' );
+ };
+
+ // Triggering action events before attaching should do nothing
+ // use of focus() would run into jQuery bug #14740 and similar issues
+ embed.embedTextHtml.$element.find( 'textarea' ).triggerHandler( 'focus' );
+ embed.embedTextWikitext.$element.find( 'textarea' ).triggerHandler( 'focus' );
+ embed.embedSwitch.emit( 'select' );
+ embed.embedSizeSwitchHtml.getMenu().emit(
+ 'choose', embed.embedSizeSwitchHtml.getMenu().findSelectedItem() );
+ embed.embedSizeSwitchWikitext.getMenu().emit(
+ 'choose', embed.embedSizeSwitchWikitext.getMenu().findSelectedItem() );
+
+ embed.selectAllOnEvent = function () {
+ assert.ok( true, 'selectAllOnEvent was called.' );
+ };
+ embed.handleTypeSwitch = function () {
+ assert.ok( true, 'handleTypeSwitch was called.' );
+ };
+ embed.handleSizeSwitch = function () {
+ assert.ok( true, 'handleTypeSwitch was called.' );
+ };
+
+ embed.attach();
+
+ // Action events should be handled now
+ embed.embedTextHtml.$element.find( 'textarea' ).triggerHandler( 'focus' );
+ embed.embedTextWikitext.$element.find( 'textarea' ).triggerHandler( 'focus' );
+ embed.embedSwitch.emit( 'select' );
+ embed.embedSizeSwitchHtml.getMenu().emit(
+ 'choose', embed.embedSizeSwitchHtml.getMenu().findSelectedItem() );
+ embed.embedSizeSwitchWikitext.getMenu().emit(
+ 'choose', embed.embedSizeSwitchWikitext.getMenu().findSelectedItem() );
+
+ // Test the unattach part
+ embed.selectAllOnEvent = function () {
+ assert.ok( false, 'selectAllOnEvent should not have been called.' );
+ };
+ embed.handleTypeSwitch = function () {
+ assert.ok( false, 'handleTypeSwitch should not have been called.' );
+ };
+ embed.handleSizeSwitch = function () {
+ assert.ok( false, 'handleTypeSwitch should not have been called.' );
+ };
+
+ embed.unattach();
+
+ // Triggering action events now that we are unattached should do nothing
+ embed.embedTextHtml.$element.find( 'textarea' ).triggerHandler( 'focus' );
+ embed.embedTextWikitext.$element.find( 'textarea' ).triggerHandler( 'focus' );
+ embed.embedSwitch.emit( 'select' );
+ embed.embedSizeSwitchHtml.getMenu().emit(
+ 'choose', embed.embedSizeSwitchHtml.getMenu().findSelectedItem() );
+ embed.embedSizeSwitchWikitext.getMenu().emit(
+ 'choose', embed.embedSizeSwitchWikitext.getMenu().findSelectedItem() );
+ } );
+
+ QUnit.test( 'handleTypeSwitch():', function ( assert ) {
+ var embed = new mw.mmv.ui.reuse.Embed( $qf );
+
+ assert.strictEqual( embed.isSizeMenuDefaultReset, false, 'Reset flag intialized correctly.' );
+
+ embed.resetCurrentSizeMenuToDefault = function () {
+ assert.ok( true, 'resetCurrentSizeMenuToDefault() called.' );
+ };
+
+ // HTML selected
+ embed.handleTypeSwitch( { getData: function () { return 'html'; } } );
+
+ assert.strictEqual( embed.isSizeMenuDefaultReset, true, 'Reset flag updated correctly.' );
+ assert.ok( !embed.embedSizeSwitchWikitext.getMenu().isVisible(), 'Wikitext size menu should be hidden.' );
+
+ embed.resetCurrentSizeMenuToDefault = function () {
+ assert.ok( false, 'resetCurrentSizeMenuToDefault() should not have been called.' );
+ };
+
+ // Wikitext selected, we are done resetting defaults
+ embed.handleTypeSwitch( { getData: function () { return 'wikitext'; } } );
+
+ assert.strictEqual( embed.isSizeMenuDefaultReset, true, 'Reset flag updated correctly.' );
+ assert.ok( !embed.embedSizeSwitchHtml.getMenu().isVisible(), 'HTML size menu should be hidden.' );
+ } );
+
+ QUnit.test( 'Logged out', function ( assert ) {
+ var embed,
+ oldUserIsAnon = mw.user.isAnon;
+
+ mw.user.isAnon = function () { return true; };
+
+ embed = new mw.mmv.ui.reuse.Embed( $qf );
+
+ assert.ok( !embed.embedSizeSwitchWikitext.$element.hasClass( 'active' ), 'Wikitext widget should be hidden.' );
+ assert.ok( embed.embedSizeSwitchHtml.$element.hasClass( 'active' ), 'HTML widget should be visible.' );
+ assert.ok( !embed.embedTextWikitext.$element.hasClass( 'active' ), 'Wikitext input should be hidden.' );
+ assert.ok( embed.embedTextHtml.$element.hasClass( 'active' ), 'HTML input should be visible.' );
+
+ mw.user.isAnon = oldUserIsAnon;
+ } );
+
+}( mediaWiki, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.reuse.share.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.reuse.share.test.js
new file mode 100644
index 00000000..5757d35b
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.reuse.share.test.js
@@ -0,0 +1,95 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $ ) {
+ function makeShare() {
+ return new mw.mmv.ui.reuse.Share( $( '#qunit-fixture' ) );
+ }
+
+ QUnit.module( 'mmv.ui.reuse.share', QUnit.newMwEnvironment() );
+
+ QUnit.test( 'Sanity test, object creation and UI construction', function ( assert ) {
+ var share = makeShare();
+
+ assert.ok( share, 'Share UI element is created.' );
+ assert.strictEqual( share.$pane.length, 1, 'Pane div created.' );
+ assert.ok( share.pageInput, 'Text field created.' );
+ assert.ok( share.$pageLink, 'Link created.' );
+ } );
+
+ QUnit.test( 'set()/empty():', function ( assert ) {
+ var share = makeShare(),
+ image = { // fake mw.mmv.model.Image
+ title: new mw.Title( 'File:Foobar.jpg' ),
+ url: 'https://upload.wikimedia.org/wikipedia/commons/3/3a/Foobar.jpg',
+ descriptionUrl: '//commons.wikimedia.org/wiki/File:Foobar.jpg'
+ };
+
+ assert.notStrictEqual( !share.pageInput.getValue(), '', 'pageInput is empty.' );
+
+ share.select = function () {
+ assert.ok( true, 'Text has been selected after data is set.' );
+ };
+
+ share.set( image );
+
+ assert.notStrictEqual( share.pageInput.getValue(), '', 'pageInput is not empty.' );
+
+ share.empty();
+
+ assert.notStrictEqual( !share.pageInput.getValue(), '', 'pageInput is empty.' );
+ } );
+
+ QUnit.test( 'attach()/unattach():', function ( assert ) {
+ var share = makeShare(),
+ image = {
+ title: new mw.Title( 'File:Foobar.jpg' ),
+ url: 'https://upload.wikimedia.org/wikipedia/commons/3/3a/Foobar.jpg',
+ descriptionUrl: '//commons.wikimedia.org/wiki/File:Foobar.jpg'
+ };
+
+ share.set( image );
+
+ share.selectAllOnEvent = function () {
+ assert.ok( false, 'selectAllOnEvent should not have been called.' );
+ };
+
+ // Triggering action events before attaching should do nothing
+ // use of focus() would run into jQuery bug #14740 and similar issues
+ share.pageInput.$element.find( 'input' ).triggerHandler( 'focus' );
+
+ share.selectAllOnEvent = function () {
+ assert.ok( true, 'selectAllOnEvent was called.' );
+ };
+
+ share.attach();
+
+ // Action events should be handled now
+ share.pageInput.$element.find( 'input' ).triggerHandler( 'focus' );
+
+ // Test the unattach part
+ share.selectAllOnEvent = function () {
+ assert.ok( false, 'selectAllOnEvent should not have been called.' );
+ };
+
+ share.unattach();
+
+ // Triggering action events now that we are unattached should do nothing
+ share.pageInput.$element.find( 'input' ).triggerHandler( 'focus' );
+ } );
+
+}( mediaWiki, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.reuse.tab.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.reuse.tab.test.js
new file mode 100644
index 00000000..9bc7c4fa
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.reuse.tab.test.js
@@ -0,0 +1,43 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $ ) {
+ var $fixture = $( '#qunit-fixture' );
+
+ function makeReuseTab() {
+ return new mw.mmv.ui.reuse.Tab( $( '<div>' ).appendTo( $fixture ), $fixture );
+ }
+
+ QUnit.module( 'mmv.ui.reuse.Tab', QUnit.newMwEnvironment() );
+
+ QUnit.test( 'Object creation, UI construction and basic funtionality', function ( assert ) {
+ var reuseTab = makeReuseTab();
+
+ assert.ok( reuseTab, 'Reuse UI element is created.' );
+ assert.strictEqual( reuseTab.$pane.length, 1, 'Pane created.' );
+
+ assert.ok( !reuseTab.$pane.hasClass( 'active' ), 'Tab is not active.' );
+
+ reuseTab.show();
+
+ assert.ok( reuseTab.$pane.hasClass( 'active' ), 'Tab is active.' );
+
+ reuseTab.hide();
+
+ assert.ok( !reuseTab.$pane.hasClass( 'active' ), 'Tab is not active.' );
+ } );
+}( mediaWiki, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.reuse.utils.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.reuse.utils.test.js
new file mode 100644
index 00000000..07967dd9
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.reuse.utils.test.js
@@ -0,0 +1,117 @@
+/*
+ * This file is part of the MediaWiki extension MultimediaViewer.
+ *
+ * MultimediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MultimediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $ ) {
+ QUnit.module( 'mw.mmv.ui.reuse.utils', QUnit.newMwEnvironment() );
+
+ QUnit.test( 'Sanity test, object creation and UI construction', function ( assert ) {
+ var utils = new mw.mmv.ui.Utils();
+
+ assert.ok( utils, 'ReuseUtils object is created.' );
+ } );
+
+ QUnit.test( 'createPulldownMenu():', function ( assert ) {
+ var utils = new mw.mmv.ui.Utils(),
+ menuItems = [ 'original', 'small', 'medium', 'large' ],
+ def = 'large',
+ menu = utils.createPulldownMenu(
+ menuItems,
+ [ 'mw-mmv-download-size' ],
+ def
+ ),
+ options = menu.getMenu().getItems(),
+ i, data;
+
+ assert.strictEqual( options.length, 4, 'Menu has correct number of items.' );
+
+ for ( i = 0; i < menuItems.length; i++ ) {
+ data = options[ i ].getData();
+
+ assert.strictEqual( data.name, menuItems[ i ], 'Correct item name on the list.' );
+ assert.strictEqual( data.height, null, 'Correct item height on the list.' );
+ assert.strictEqual( data.width, null, 'Correct item width on the list.' );
+ }
+
+ assert.strictEqual( menu.getMenu().findSelectedItem(), options[ 3 ], 'Default set correctly.' );
+ } );
+
+ QUnit.test( 'updateMenuOptions():', function ( assert ) {
+ var utils = new mw.mmv.ui.Utils(),
+ menu = utils.createPulldownMenu(
+ [ 'original', 'small', 'medium', 'large' ],
+ [ 'mw-mmv-download-size' ],
+ 'original'
+ ),
+ options = menu.getMenu().getItems(),
+ width = 700,
+ height = 500,
+ sizes = utils.getPossibleImageSizesForHtml( width, height ),
+ oldMessage = mw.message;
+
+ mw.message = function ( messageKey ) {
+ assert.ok( messageKey.match( /^multimediaviewer-(small|medium|original|embed-dimensions)/ ), 'messageKey passed correctly.' );
+
+ return { text: $.noop };
+ };
+
+ utils.updateMenuOptions( sizes, options );
+
+ mw.message = oldMessage;
+ } );
+
+ QUnit.test( 'getPossibleImageSizesForHtml()', function ( assert ) {
+ var utils = new mw.mmv.ui.Utils(),
+ exampleSizes = [
+ // Big wide image
+ {
+ width: 2048, height: 1536,
+ expected: {
+ small: { width: 193, height: 145 },
+ medium: { width: 640, height: 480 },
+ large: { width: 1200, height: 900 },
+ original: { width: 2048, height: 1536 }
+ }
+ },
+
+ // Big tall image
+ {
+ width: 201, height: 1536,
+ expected: {
+ small: { width: 19, height: 145 },
+ medium: { width: 63, height: 480 },
+ large: { width: 118, height: 900 },
+ original: { width: 201, height: 1536 }
+ }
+ },
+
+ // Very small image
+ {
+ width: 15, height: 20,
+ expected: {
+ original: { width: 15, height: 20 }
+ }
+ }
+ ],
+ i, cursize, opts;
+ for ( i = 0; i < exampleSizes.length; i++ ) {
+ cursize = exampleSizes[ i ];
+ opts = utils.getPossibleImageSizesForHtml( cursize.width, cursize.height );
+ assert.deepEqual( opts, cursize.expected, 'We got the expected results out of the size calculation function.' );
+ }
+ } );
+
+}( mediaWiki, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.stripeButtons.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.stripeButtons.test.js
new file mode 100644
index 00000000..7ebe04e9
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.stripeButtons.test.js
@@ -0,0 +1,76 @@
+/*
+ * This file is part of the MediaWiki extension MediaViewer.
+ *
+ * MediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $ ) {
+ QUnit.module( 'mmv.ui.StripeButtons', QUnit.newMwEnvironment() );
+
+ function createStripeButtons() {
+ var fixture = $( '#qunit-fixture' );
+ return new mw.mmv.ui.StripeButtons( fixture );
+ }
+
+ QUnit.test( 'Sanity test, object creation and UI construction', function ( assert ) {
+ var buttons,
+ oldMwUserIsAnon = mw.user.isAnon;
+
+ // first pretend we are anonymous
+ mw.user.isAnon = function () { return true; };
+ buttons = createStripeButtons();
+
+ assert.ok( buttons, 'UI element is created.' );
+ assert.ok( buttons.buttons.$descriptionPage, 'File page button created for anon.' );
+
+ // now pretend we are logged in
+ mw.user.isAnon = function () { return false; };
+ buttons = createStripeButtons();
+
+ assert.strictEqual( buttons.buttons.$descriptionPage.length, 1, 'File page button created for logged in.' );
+
+ mw.user.isAnon = oldMwUserIsAnon;
+ } );
+
+ QUnit.test( 'set()/empty() sanity test:', function ( assert ) {
+ var buttons = createStripeButtons(),
+ fakeImageInfo = { descriptionUrl: '//commons.wikimedia.org/wiki/File:Foo.jpg' },
+ fakeRepoInfo = { displayName: 'Wikimedia Commons', isCommons: function () { return true; } };
+
+ buttons.set( fakeImageInfo, fakeRepoInfo );
+ buttons.empty();
+
+ assert.ok( true, 'No error on set()/empty().' );
+ } );
+
+ QUnit.test( 'Description page button', function ( assert ) {
+ var $qf = $( '#qunit-fixture' ),
+ buttons = new mw.mmv.ui.StripeButtons( $qf ),
+ button = buttons.buttons.$descriptionPage,
+ descriptionUrl = 'http://example.com/desc',
+ imageInfo = { descriptionUrl: descriptionUrl },
+ repoInfo = { isCommons: function () { return false; } };
+
+ buttons.setDescriptionPageButton( imageInfo, repoInfo );
+
+ assert.ok( !button.hasClass( 'mw-mmv-repo-button-commons' ), 'Button does not have commons class non-Commons files' );
+ assert.strictEqual( button.find( 'a' ).addBack().filter( 'a' ).attr( 'href' ), descriptionUrl, 'Description page link is correct' );
+
+ repoInfo.isCommons = function () { return true; };
+ buttons.setDescriptionPageButton( imageInfo, repoInfo );
+
+ assert.ok( button.hasClass( 'mw-mmv-repo-button-commons' ), 'Button commons class for Commons files' );
+ } );
+
+}( mediaWiki, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.test.js
new file mode 100644
index 00000000..7f78a060
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.test.js
@@ -0,0 +1,109 @@
+( function ( mw, $ ) {
+ QUnit.module( 'mmv.ui', QUnit.newMwEnvironment( {
+ setup: function () {
+ this.clock = this.sandbox.useFakeTimers();
+ }
+ } ) );
+
+ QUnit.test( 'handleEvent()', function ( assert ) {
+ var element = new mw.mmv.ui.Element( $( '<div>' ) );
+
+ element.handleEvent( 'mmv-foo', function () {
+ assert.ok( true, 'Event is handled' );
+ } );
+
+ $( document ).trigger( new $.Event( 'mmv-foo' ) );
+
+ element.clearEvents();
+
+ $( document ).trigger( new $.Event( 'mmv-foo' ) );
+ } );
+
+ QUnit.test( 'setInlineStyle()', function ( assert ) {
+ var element = new mw.mmv.ui.Element( $( '<div>' ) ),
+ $testDiv = $( '<div id="mmv-testdiv">!!!</div>' ).appendTo( '#qunit-fixture' );
+
+ assert.ok( $testDiv.is( ':visible' ), 'Test div is visible' );
+
+ element.setInlineStyle( 'test', '#mmv-testdiv { display: none; }' );
+
+ assert.ok( !$testDiv.is( ':visible' ), 'Test div is hidden by inline style' );
+
+ element.setInlineStyle( 'test', null );
+
+ assert.ok( $testDiv.is( ':visible' ), 'Test div is visible again' );
+ } );
+
+ QUnit.test( 'setTimer()/clearTimer()/resetTimer()', function ( assert ) {
+ var element = new mw.mmv.ui.Element( $( '<div>' ) ),
+ element2 = new mw.mmv.ui.Element( $( '<div>' ) ),
+ spy = this.sandbox.spy(),
+ spy2 = this.sandbox.spy();
+
+ element.setTimer( 'foo', spy, 10 );
+ this.clock.tick( 100 );
+ assert.ok( spy.called, 'Timeout callback was called' );
+ assert.ok( spy.calledOnce, 'Timeout callback was called once' );
+ assert.ok( spy.calledOn( element ), 'Timeout callback was called on the element' );
+
+ spy = this.sandbox.spy();
+ element.setTimer( 'foo', spy, 10 );
+ element.setTimer( 'foo', spy2, 20 );
+ this.clock.tick( 100 );
+ assert.ok( !spy.called, 'Old timeout callback was not called after update' );
+ assert.ok( spy2.called, 'New timeout callback was called after update' );
+
+ spy = this.sandbox.spy();
+ spy2 = this.sandbox.spy();
+ element.setTimer( 'foo', spy, 10 );
+ element.setTimer( 'bar', spy2, 20 );
+ this.clock.tick( 100 );
+ assert.ok( spy.called && spy2.called, 'Timeouts with different names do not conflict' );
+
+ spy = this.sandbox.spy();
+ spy2 = this.sandbox.spy();
+ element.setTimer( 'foo', spy, 10 );
+ element2.setTimer( 'foo', spy2, 20 );
+ this.clock.tick( 100 );
+ assert.ok( spy.called && spy2.called, 'Timeouts in different elements do not conflict' );
+
+ spy = this.sandbox.spy();
+ element.setTimer( 'foo', spy, 10 );
+ element.clearTimer( 'foo' );
+ this.clock.tick( 100 );
+ assert.ok( !spy.called, 'Timeout is invalidated by clearing' );
+
+ spy = this.sandbox.spy();
+ element.setTimer( 'foo', spy, 100 );
+ this.clock.tick( 80 );
+ element.resetTimer( 'foo' );
+ this.clock.tick( 80 );
+ assert.ok( !spy.called, 'Timeout is reset' );
+ this.clock.tick( 80 );
+ assert.ok( spy.called, 'Timeout works after reset' );
+
+ spy = this.sandbox.spy();
+ element.setTimer( 'foo', spy, 100 );
+ this.clock.tick( 80 );
+ element.resetTimer( 'foo', 200 );
+ this.clock.tick( 180 );
+ assert.ok( !spy.called, 'Timeout is reset to the designated delay' );
+ this.clock.tick( 80 );
+ assert.ok( spy.called, 'Timeout works after changing the delay' );
+ } );
+
+ QUnit.test( 'correctEW()', function ( assert ) {
+ var element = new mw.mmv.ui.Element( $( '<div>' ) );
+
+ element.isRTL = this.sandbox.stub().returns( true );
+
+ assert.strictEqual( element.correctEW( 'e' ), 'w', 'e (east) is flipped' );
+ assert.strictEqual( element.correctEW( 'ne' ), 'nw', 'ne (northeast) is flipped' );
+ assert.strictEqual( element.correctEW( 'W' ), 'E', 'uppercase is flipped' );
+ assert.strictEqual( element.correctEW( 's' ), 's', 'non-horizontal directions are ignored' );
+
+ element.isRTL.returns( false );
+
+ assert.strictEqual( element.correctEW( 'e' ), 'e', 'no flipping in LTR documents' );
+ } );
+}( mediaWiki, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.tipsyDialog.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.tipsyDialog.test.js
new file mode 100644
index 00000000..3d4044ec
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.tipsyDialog.test.js
@@ -0,0 +1,68 @@
+/*
+ * This file is part of the MediaWiki extension MediaViewer.
+ *
+ * MediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $ ) {
+ QUnit.module( 'mmv.ui.tipsyDialog', QUnit.newMwEnvironment( {
+ setup: function () {
+ // remove tipsy elements left behind by other tests so these tests don't find them by accident
+ // tipsy puts its elements to the end of the body so clearing the fixture doesn't get rid of them
+ $( '.mw-mmv-tipsy-dialog' ).remove();
+ }
+ } ) );
+
+ QUnit.test( 'Open/close', function ( assert ) {
+ var $qf = $( '#qunit-fixture' ),
+ $anchor = $( '<div>' ).appendTo( $qf ),
+ dialog = new mw.mmv.ui.TipsyDialog( $anchor );
+
+ assert.ok( !$( '.mw-mmv-tipsy-dialog' ).length, 'dialog is not shown' );
+ dialog.open();
+ assert.ok( $( '.mw-mmv-tipsy-dialog' ).length, 'dialog is shown' );
+ dialog.close();
+ assert.ok( !$( '.mw-mmv-tipsy-dialog' ).length, 'dialog is not shown' );
+ } );
+
+ QUnit.test( 'setContent', function ( assert ) {
+ var $qf = $( '#qunit-fixture' ),
+ $anchor = $( '<div>' ).appendTo( $qf ),
+ titleText = 'This is a title',
+ bodyText = 'This is the <b class="typsyDialogTest-123">body</b>',
+ dialog = new mw.mmv.ui.TipsyDialog( $anchor );
+
+ dialog.setContent( titleText, bodyText );
+ dialog.open();
+ assert.ok( $( '.mw-mmv-tipsy-dialog' ).text().match( titleText ), 'Title is included' );
+ assert.ok( $( '.mw-mmv-tipsy-dialog' ).html().match( bodyText ), 'Body is included' );
+ assert.ok( $( '.mw-mmv-tipsy-dialog' ).find( '.typsyDialogTest-123' ).length, 'Body is HTML' );
+ } );
+
+ QUnit.test( 'Close on click', function ( assert ) {
+ var $qf = $( '#qunit-fixture' ),
+ $anchor = $( '<div>' ).appendTo( $qf ),
+ dialog = new mw.mmv.ui.TipsyDialog( $anchor );
+
+ dialog.open();
+ assert.ok( $( '.mw-mmv-tipsy-dialog' ).length, 'dialog is shown initially' );
+ dialog.getPopup().click();
+ assert.ok( $( '.mw-mmv-tipsy-dialog' ).length, 'dialog is not hidden when clicked' );
+ dialog.getPopup().find( '.mw-mmv-tipsy-dialog-disable' ).click();
+ assert.ok( !$( '.mw-mmv-tipsy-dialog' ).length, 'dialog is hidden when close icon is clicked' );
+ dialog.open();
+ $qf.click();
+ assert.ok( !$( '.mw-mmv-tipsy-dialog' ).length, 'dialog is hidden when clicked outside' );
+ } );
+}( mediaWiki, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.truncatableTextField.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.truncatableTextField.test.js
new file mode 100644
index 00000000..6516b2b6
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.truncatableTextField.test.js
@@ -0,0 +1,64 @@
+/*
+ * This file is part of the MediaWiki extension MediaViewer.
+ *
+ * MediaViewer is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MediaViewer is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MediaViewer. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+( function ( mw, $ ) {
+ QUnit.module( 'mmv.ui.TruncatableTextField', QUnit.newMwEnvironment() );
+
+ /**
+ * Create a textfield that can contain exactly width x height characters
+ *
+ * @param {number} width
+ * @param {number} height
+ * @param {jQuery} $qf fixture element
+ * @param {Object} sandbox sinon instance
+ * @return {TruncatableTextField}
+ */
+ function getField( width, height, $qf, sandbox ) {
+ var $container = $( '<div>' ).appendTo( $qf ),
+ $element = $( '<span>' ),
+ ttf = new mw.mmv.ui.TruncatableTextField( $container, $element, {} );
+
+ ttf.htmlUtils.htmlToTextWithLinks = sandbox.stub().returnsArg( 0 );
+
+ $container.css( {
+ fontFamily: 'monospace',
+ lineHeight: 1,
+ width: width + 'ch',
+ height: height + 'em'
+ } );
+
+ return ttf;
+ }
+
+ QUnit.test( 'Normal constructor', function ( assert ) {
+ var $container = $( '#qunit-fixture' ),
+ $element = $( '<div>' ).appendTo( $container ).text( 'This is a unique string.' ),
+ ttf = new mw.mmv.ui.TruncatableTextField( $container, $element );
+
+ assert.strictEqual( ttf.$element.text(), 'This is a unique string.', 'The constructor set the element to the right thing.' );
+ assert.strictEqual( ttf.$element.closest( '#qunit-fixture' ).length, 1, 'The constructor put the element into the container.' );
+ } );
+
+ QUnit.test( 'Set method', function ( assert ) {
+ var $qf = $( '#qunit-fixture' ),
+ ttf = getField( 3, 2, $qf, this.sandbox );
+
+ ttf.shrink = this.sandbox.stub();
+ ttf.set( 'abc' );
+ assert.strictEqual( ttf.$element.text(), 'abc', 'Text is set accurately.' );
+ } );
+}( mediaWiki, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.viewingOptions.test.js b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.viewingOptions.test.js
new file mode 100644
index 00000000..ee1a9e29
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/tests/qunit/mmv/ui/mmv.ui.viewingOptions.test.js
@@ -0,0 +1,139 @@
+( function ( mw, $ ) {
+ function makeDialog( initialise ) {
+ var $qf = $( '#qunit-fixture' ),
+ $button = $( '<div>' ).appendTo( $qf ),
+ dialog = new mw.mmv.ui.OptionsDialog( $qf, $button, { setMediaViewerEnabledOnClick: $.noop } );
+
+ if ( initialise ) {
+ dialog.initPanel();
+ }
+
+ return dialog;
+ }
+
+ QUnit.module( 'mmv.ui.viewingOptions', QUnit.newMwEnvironment() );
+
+ QUnit.test( 'Constructor sanity test', function ( assert ) {
+ var dialog = makeDialog();
+ assert.ok( dialog, 'Dialog is created successfully' );
+ } );
+
+ QUnit.test( 'Initialisation functions', function ( assert ) {
+ var dialog = makeDialog( true );
+
+ assert.ok( dialog.$disableDiv, 'Disable div is created.' );
+ assert.ok( dialog.$enableDiv, 'Enable div is created.' );
+ assert.ok( dialog.$disableConfirmation, 'Disable confirmation is created.' );
+ assert.ok( dialog.$enableConfirmation, 'Enable confirmation is created.' );
+ } );
+
+ QUnit.test( 'Disable', function ( assert ) {
+ var $header, $icon, $text, $textHeader, $textBody,
+ $submitButton, $cancelButton, $aboutLink,
+ dialog = makeDialog(),
+ deferred = $.Deferred();
+
+ this.sandbox.stub( dialog.config, 'setMediaViewerEnabledOnClick', function () {
+ return deferred;
+ } );
+
+ dialog.initDisableDiv();
+
+ $header = dialog.$disableDiv.find( 'h3.mw-mmv-options-dialog-header' );
+ $icon = dialog.$disableDiv.find( 'div.mw-mmv-options-icon' );
+
+ $text = dialog.$disableDiv.find( 'div.mw-mmv-options-text' );
+ $textHeader = $text.find( 'p.mw-mmv-options-text-header' );
+ $textBody = $text.find( 'p.mw-mmv-options-text-body' );
+ $aboutLink = $text.find( 'a.mw-mmv-project-info-link' );
+ $submitButton = dialog.$disableDiv.find( 'button.mw-mmv-options-submit-button' );
+ $cancelButton = dialog.$disableDiv.find( 'button.mw-mmv-options-cancel-button' );
+
+ assert.strictEqual( $header.length, 1, 'Disable header created successfully.' );
+ assert.strictEqual( $header.text(), 'Disable Media Viewer?', 'Disable header has correct text (if this fails, it may be due to i18n differences)' );
+
+ assert.strictEqual( $icon.length, 1, 'Icon created successfully.' );
+ assert.strictEqual( $icon.html(), '&nbsp;', 'Icon has a blank space in it.' );
+
+ assert.ok( $text, 'Text div created successfully.' );
+ assert.strictEqual( $textHeader.length, 1, 'Text header created successfully.' );
+ assert.strictEqual( $textHeader.text(), 'Skip this viewing feature for all files.', 'Text header has correct text (if this fails, it may be due to i18n differences)' );
+
+ assert.strictEqual( $textBody.length, 1, 'Text body created successfully.' );
+ assert.strictEqual( $textBody.text(), 'You can enable it later through the file details page.', 'Text body has correct text (if this fails, it may be due to i18n differences)' );
+
+ assert.strictEqual( $aboutLink.length, 1, 'About link created successfully.' );
+ assert.strictEqual( $aboutLink.text(), 'Learn more', 'About link has correct text (if this fails, it may be due to i18n differences)' );
+
+ assert.strictEqual( $submitButton.length, 1, 'Disable button created successfully.' );
+ assert.strictEqual( $submitButton.text(), 'Disable Media Viewer', 'Disable button has correct text (if this fails, it may be due to i18n differences)' );
+
+ assert.strictEqual( $cancelButton.length, 1, 'Cancel button created successfully.' );
+ assert.strictEqual( $cancelButton.text(), 'Cancel', 'Cancel button has correct text (if this fails, it may be due to i18n differences)' );
+
+ $submitButton.click();
+
+ assert.ok( !dialog.$disableConfirmation.hasClass( 'mw-mmv-shown' ), 'Disable confirmation not shown yet' );
+ assert.ok( !dialog.$dialog.hasClass( 'mw-mmv-disable-confirmation-shown' ), 'Disable confirmation not shown yet' );
+
+ // Pretend that the async call in mmv.js succeeded
+ deferred.resolve();
+
+ // The confirmation should appear
+ assert.ok( dialog.$disableConfirmation.hasClass( 'mw-mmv-shown' ), 'Disable confirmation shown' );
+ assert.ok( dialog.$dialog.hasClass( 'mw-mmv-disable-confirmation-shown' ), 'Disable confirmation shown' );
+ } );
+
+ QUnit.test( 'Enable', function ( assert ) {
+ var $header, $icon, $text, $textHeader, $aboutLink,
+ $submitButton, $cancelButton,
+ dialog = makeDialog(),
+ deferred = $.Deferred();
+
+ this.sandbox.stub( dialog.config, 'setMediaViewerEnabledOnClick', function () {
+ return deferred;
+ } );
+
+ dialog.initEnableDiv();
+
+ $header = dialog.$enableDiv.find( 'h3.mw-mmv-options-dialog-header' );
+ $icon = dialog.$enableDiv.find( 'div.mw-mmv-options-icon' );
+
+ $text = dialog.$enableDiv.find( 'div.mw-mmv-options-text' );
+ $textHeader = $text.find( 'p.mw-mmv-options-text-header' );
+ $aboutLink = $text.find( 'a.mw-mmv-project-info-link' );
+ $submitButton = dialog.$enableDiv.find( 'button.mw-mmv-options-submit-button' );
+ $cancelButton = dialog.$enableDiv.find( 'button.mw-mmv-options-cancel-button' );
+
+ assert.strictEqual( $header.length, 1, 'Enable header created successfully.' );
+ assert.strictEqual( $header.text(), 'Enable Media Viewer?', 'Enable header has correct text (if this fails, it may be due to i18n differences)' );
+
+ assert.strictEqual( $icon.length, 1, 'Icon created successfully.' );
+ assert.strictEqual( $icon.html(), '&nbsp;', 'Icon has a blank space in it.' );
+
+ assert.ok( $text, 'Text div created successfully.' );
+ assert.strictEqual( $textHeader.length, 1, 'Text header created successfully.' );
+ assert.strictEqual( $textHeader.text(), 'Enable this media viewing feature for all files by default.', 'Text header has correct text (if this fails, it may be due to i18n differences)' );
+
+ assert.strictEqual( $aboutLink.length, 1, 'About link created successfully.' );
+ assert.strictEqual( $aboutLink.text(), 'Learn more', 'About link has correct text (if this fails, it may be due to i18n differences)' );
+
+ assert.strictEqual( $submitButton.length, 1, 'Enable button created successfully.' );
+ assert.strictEqual( $submitButton.text(), 'Enable Media Viewer', 'Enable button has correct text (if this fails, it may be due to i18n differences)' );
+
+ assert.strictEqual( $cancelButton.length, 1, 'Cancel button created successfully.' );
+ assert.strictEqual( $cancelButton.text(), 'Cancel', 'Cancel button has correct text (if this fails, it may be due to i18n differences)' );
+
+ $submitButton.click();
+
+ assert.ok( !dialog.$enableConfirmation.hasClass( 'mw-mmv-shown' ), 'Enable confirmation not shown yet' );
+ assert.ok( !dialog.$dialog.hasClass( 'mw-mmv-enable-confirmation-shown' ), 'Enable confirmation not shown yet' );
+
+ // Pretend that the async call in mmv.js succeeded
+ deferred.resolve();
+
+ // The confirmation should appear
+ assert.ok( dialog.$enableConfirmation.hasClass( 'mw-mmv-shown' ), 'Enable confirmation shown' );
+ assert.ok( dialog.$dialog.hasClass( 'mw-mmv-enable-confirmation-shown' ), 'Enable confirmation shown' );
+ } );
+}( mediaWiki, jQuery ) );
diff --git a/www/wiki/extensions/MultimediaViewer/version b/www/wiki/extensions/MultimediaViewer/version
new file mode 100644
index 00000000..0ba061c1
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/version
@@ -0,0 +1,4 @@
+MultimediaViewer: REL1_31
+2018-05-11T20:11:31
+
+1273d3e
diff --git a/www/wiki/extensions/MultimediaViewer/viewer-ltr.svg b/www/wiki/extensions/MultimediaViewer/viewer-ltr.svg
new file mode 100644
index 00000000..b42b96c9
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/viewer-ltr.svg
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="264" height="162" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 264 162">
+ <defs>
+ <path id="a" fill="#eaecf0" d="M0 5h22v6H0z"/>
+ </defs>
+ <path fill="#fff" d="M24.833 151.362l-11.467 9.995L.5 151.749V.5h263v151.749l-9 6.705V42.5h-39v112.6l-4.393-3.731-7.607 5.33V42.5h-155v110.849l-9.751 8.031"/>
+ <path fill="#eaecf0" d="M263 1v150.998l-8 5.96V42h-40v112.019l-3.274-2.779-.589-.499-.633.44-6.504 4.557V42H48v111.099l-9.257 7.661-13.295-9.569-.644-.465-.598.521-10.864 9.47L1 151.499V1h262m1-1H0v152l13.391 10 11.473-10 13.891 10L49 153.6V43h154v114.66l8.078-5.66 4.922 4.18V43h38v116.95l10-7.45V0z"/>
+ <path fill="#eaecf0" d="M203 157.66V43H49v110.6l2.145-1.6L63.7 162l13.809-10 14.229 10 12.972-10 12.973 10 13.811-10 12.136 10 13.391-10 14.229 10 12.972-10 12.974 10 5.804-4.34zM11 36c0-7.732 6.268-14 14-14s14 6.268 14 14-6.268 14-14 14-14-6.268-14-14zm243 123.95V43h-38v113.18l7.551 5.82 13.811-10 13.891 10 2.747-2.05zM38 107V72H13v35h25zM232.51 5h26v6h-26V5z"/>
+ <use x="208.51" xlink:href="#a"/>
+ <path fill="#eaecf0" d="M142 6v4H50V6h92m1-1H49v6h94V5z"/>
+ <use x="184.51" xlink:href="#a"/>
+ <use width="13" x="161.51" xlink:href="#a"/>
+ <use width="6" x="176.51" xlink:href="#a"/>
+ <use width="6" x="153.51" xlink:href="#a"/>
+ <use width="32" x="9" xlink:href="#a"/>
+ <path fill="#eaecf0" stroke="#eaecf0" d="M2 14.5h260"/>
+ <path fill="#a2a9b1" d="M221 73h24v1h-24v-1z"/>
+ <use width="2" height="2" x="52" y="2"/>
+ <path fill="#eaecf0" d="M38 59v-5H13v5h25z"/>
+ <path fill="#8acdff" d="M250 47h-30v24h30V47zm-20.268 11h1.08l6.69 6.596 3.634-2.172 5.477 4.557V67h-23.959l7.078-9z"/>
+ <path fill="#fff" d="M54 15h155v128H54V15z"/>
+ <path fill="#36c" d="M204 24H60v112h144V24zm-97.679 50h5.184l32.129 34.005 17.446-10.42 26.28 21.317V119H72.343l33.978-45z"/>
+ <path fill="#fff" d="M191.795 34.937L187.955 31H198v10.067l-3.948-4.221-6.478 6.494L191.21 47H182V36.933l3.637 4.046 6.158-6.042z"/>
+</svg>
diff --git a/www/wiki/extensions/MultimediaViewer/viewer-rtl.svg b/www/wiki/extensions/MultimediaViewer/viewer-rtl.svg
new file mode 100644
index 00000000..e403ab73
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/viewer-rtl.svg
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="264" height="162" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 264 162">
+ <defs>
+ <path id="a" fill="#eaecf0" d="M0 5h22v6H0z"/>
+ </defs>
+ <path fill="#fff" d="M250.634 161.357l12.866-9.608V.5H.5v151.749l9 6.705V42.5h39v112.6l4.393-3.731 7.607 5.33V42.5h155v110.849l9.751 8.031 13.916-10.018"/>
+ <path fill="#eaecf0" d="M1 1v150.998l8 5.96V42h40v112.019l3.274-2.779.589-.499.633.44L60 155.738V42h156v111.099l9.257 7.661 13.295-9.569.644-.465.598.521 10.864 9.47L263 151.499V1H1M0 0h264v152l-13.391 10-11.473-10-13.891 10L215 153.6V43H61v114.66L52.922 152 48 156.18V43H10v116.95L0 152.5V0z"/>
+ <path fill="#eaecf0" d="M61 157.66V43h154v110.6l-2.145-1.6-12.555 10-13.809-10-14.229 10-12.972-10-12.973 10-13.811-10-12.136 10-13.391-10-14.229 10-12.972-10-12.974 10L61 157.66zM253 36c0-7.732-6.268-14-14-14s-14 6.268-14 14 6.268 14 14 14 14-6.268 14-14zM10 159.95V43h38v113.18L40.449 162l-13.811-10-13.891 10L10 159.95zM226 107V72h25v35h-25zM31.49 5h-26v6h26V5z"/>
+ <use width="264" height="162" x="208.50999" xlink:href="#a" transform="matrix(-1 0 0 1 264 0)"/>
+ <path fill="#eaecf0" d="M122 6v4h92V6h-92m-1-1h94v6h-94V5z"/>
+ <use width="264" height="162" x="184.50999" xlink:href="#a" transform="matrix(-1 0 0 1 264 0)"/>
+ <use width="13" height="162" x="161.50999" xlink:href="#a" transform="matrix(-1 0 0 1 264 0)"/>
+ <use width="6" height="162" x="176.50999" xlink:href="#a" transform="matrix(-1 0 0 1 264 0)"/>
+ <use width="6" height="162" x="153.50999" xlink:href="#a" transform="matrix(-1 0 0 1 264 0)"/>
+ <use width="32" height="162" x="9" xlink:href="#a" transform="matrix(-1 0 0 1 264 0)"/>
+ <path fill="#eaecf0" stroke="#eaecf0" d="M262 14.5H2"/>
+ <path fill="#a2a9b1" d="M43 73H19v1h24v-1z"/>
+ <use width="2" height="2" x="52" y="2" transform="matrix(-1 0 0 1 264 0)"/>
+ <path fill="#eaecf0" d="M226 59v-5h25v5h-25z"/>
+ <path fill="#8acdff" d="M14 47h30v24H14V47zm20.268 11h-1.08l-6.69 6.596-3.634-2.172-5.477 4.557V67h23.959l-7.078-9z"/>
+ <path fill="#fff" d="M210 15H55v128h155V15z"/>
+ <path fill="#36c" d="M60 24h144v112H60V24zm97.679 50h-5.184l-32.129 34.005-17.446-10.42-26.28 21.317V119h115.017l-33.978-45z"/>
+ <path fill="#fff" d="M72.205 34.937L76.045 31H66v10.067l3.948-4.221 6.478 6.494L72.79 47H82V36.933l-3.637 4.046-6.158-6.042z"/>
+</svg>