summaryrefslogtreecommitdiff
path: root/www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.js
diff options
context:
space:
mode:
Diffstat (limited to 'www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.js')
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/mmv.js1031
1 files changed, 1031 insertions, 0 deletions
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 ) );