'use strict';

/**
 * Gestion du diaporama.
 *
 * @license https://www.gnu.org/licenses/gpl-3.0.html
 * @link https://www.igalerie.org/
 */
function Diaporama(query, link)
{
	// Retourne l'élément d'une icône.
	const _icon = name => { return _q(`#diaporama a[data-name="${name}"]`); }

	// Lien de lancement du diaporama.
	const _link = link;

	// Raccourcis.
	const _q = App.q, _qAll = App.qAll;

	// Paramètre GET "q".
	const _query = query;



	// Indique si une animation est en cours.
	var _animationActive = false;

	// Indique si la lecture automatique est en cours.
	var _autoActive = false;

	// Indique si le bouton de la souris est enfoncé pour la modification
	// de la durée d'affichage des fichiers en lecture automatique.
	var _autoDurationMouseDown = false;

	// Timer pour le changement de fichiers en lecture automatique.
	var _autoTimer;

	// Indique si une animation du carrousel est en cours.
	var _carouselAnimation = false;

	// Numéro de la 'page' courante du carrousel.
	var _carouselCurrentPage = 0;

	// Position courante dans le carrousel.
	var _carouselCurrentPosition = 1;

	// Plus grande position des fichiers du carrousel.
	var _carouselMaxPosition = 0;

	// Nombre maximal de vignettes à afficher dans le carrousel.
	var _carouselMaxThumbs = 0;

	// Nombre de pages du carrousel.
	var _carouselPagesCount = 0;

	// Informations utiles des fichiers du carrousel.
	var _carouselThumbs;

	// Est-ce qu'un déplacement d'image a démarré après un click ?
	var _clickMove = false;

	// Est-ce qu'un click sur une image a démarré ?
	var _clickStart = false;

	// Position du fichier courant dans la série.
	var _currentPosition = 1;

	// Carte de géolocalisation.
	var _geolocationMap;

	// Marqueur pour la géolocalisation.
	var _geolocationMarker;

	// Le diaporama a-t-il déjà été initialisé ?
	var _init = false;

	// Informations des fichiers.
	var _items = {};

	// Nombre de fichiers dans la série.
	var _itemsCount = 0;

	// Contrôle au clavier actif ?
	var _keyboardActive = true;

	// Texte localisé.
	var _l10n = {};

	// Options.
	var _options =
	{
		// Activer les animations ?
		animate: true,

		// Durée d'affichage par défaut d'un fichier en lecture automatique.
		// En secondes.
		autoDuration: 3.0,

		// Durée d'affichage maximale des fichiers en lecture automatique.
		// En secondes.
		autoDurationMax: 60,

		// Durée d'affichage minimale des fichiers en lecture automatique.
		// En secondes.
		autoDurationMin: 1,

		// Précision pour la durée d'affichage des fichiers en lecture automatique.
		// En secondes.
		autoDurationPrecision: 0.5,

		// En lecture automatique, recommencer la lecture depuis
		// le premier fichier lorsqu'on est arrivé au dernier ?
		autoLoop: false,

		// Démarrer la lecture automatique au lancement du diaporama ?
		autoStart: false,

		// Afficher par défaut le carrousel ?
		carousel: true,

		// Épaisseur de la bordure du haut du carrousel.
		carouselBorderTop: 2,

		// Durée de l'effet de transition entre les pages du carrousel.
		carouselNavDuration: 600,

		// Épaisseur de la bordure des vignettes du carrousel, en pixels.
		carouselThumbsBorder: 1,

		// Marge externe des vignettes du carrousel, en pixels.
		carouselThumbsMargin: 8,

		// Dimensions des vignettes du carrousel, en pixels.
		// Valeur minimale : 50.
		carouselThumbsSize: 80,

		// Activer le zoom au click ?
		clickZoom: true,

		// Afficher les barres de contrôle ?
		controlBars: true,

		// Lancer le diaporama en mode plein écran ?
		fullScreen: false,

		// Lancer le diaporama en mode plein écran pour mobile ?
		fullScreenMobile: true,

		// Nombre de fichiers à récupérer lors des requêtes SQL
		// avant et après la position du fichier courant dans la série,
		// ainsi qu'au début et à la fin de la série.
		itemLimit: 5,

		// Nombre de fichiers à précharger autour du fichier courant.
		itemPreload: 2,

		// Durée de l'effet de redimensionnement du fichier
		// pour le switch taille réelle / taille redimensionnée.
		// 0 pour désactiver l'animation.
		// En millisecondes.
		itemResizeDuration: 250,

		// Autoriser le contrôle au clavier ?
		keyboard: true,

		// Largeur maximale jusqu'où l'on considère un écran comme un écran mobile.
		// Sert à alléger l'interface sur les écrans de taille réduite.
		mobileMaxWidth: 599,

		// Afficher la description par dessus les images (mais pas les vidéos) ?
		overImageDescription: false,

		// Afficher le titre par dessus les images (mais pas les vidéos) ?
		overImageTitle: false,

		// Afficher les informations au lancement du diaporama ?
		showInformations: false,

		// Parties à afficher dans le panneau d'informations.
		sidebarInformations: {u:1,d:1,s:1,p:1,t:1,e:1,i:1,x:1},

		// Le panneau latéral doit-il ne pas masquer le fichier ?
		sidebarItemResize: true,

		// Durée de l'effet d'apparition des panneaux latéraux.
		// 0 pour désactiver l'animation.
		// En millisecondes.
		sidebarShowDuration: 250,

		// Activer le swipe ?
		swipe: true,

		// Distance minimale à parcourir pour changer de fichier.
		// En pixels.
		swipeDistance: 30,

		// Durée de l'effet d'animation du swipe.
		// En millisecondes.
		swipeDuration: 200,

		// Durée de l'effet de transition entre fichiers par défaut.
		// En millisecondes.
		transitionDuration: 500,

		// Effet de transition entre fichiers par défaut.
		transitionEffect: 'fade',

		// Activer le zoom à la molette de la souris ?
		zoom: true,

		// Facteur multiplicateur à chaque étape du zoom.
		zoomFactor: 1.1,

		// Limite du zoom, en pourcentage.
		// 0 pour aucune limite.
		zoomLimit: 100,

		// Activer également le zoom pour les vidéos ?
		zoomVideo: true
	};

	// Valeur CSS "overflow" pour <html> et <body>.
	var _overflow = {};

	// Paramètres du diaporama.
	var _params = {};

	// Précédentes dimensions du diaporama
	// (avant un changement de dimensions de la zone d'affichage).
	var _previousDiaporamaSize;

	// Le fichier courant est-il en taille réelle ?
	var _realsize = false;

	// Indique si un swipe est en cours.
	var _swipeActive = false;

	// Indique si un switch est en cours.
	var _switchActive = false;

	// Paramètres du zoom.
	var _zoom = {};

	// Timer pour l'affichage du niveau de zoom sur mobile.
	var _zoomTextTimer;



	/**
	 * Démarre le diaporama.
	 *
	 * @param int position
	 * @param object options
	 *
	 * @return void
	 */
	this.start = function(position, options)
	{
		const diaporama = _q('#diaporama');

		// Si le diaporama est déjà ouvert, on ne va pas plus loin.
		if (diaporama?.checkVisibility())
		{
			return;
		}

		// On supprime les barres de défilement du navigateur
		// et on désactive l'affichage de la galerie.
		_overflow =
		{
			html: window.getComputedStyle(_q('html')).getPropertyValue('overflow'),
			body: window.getComputedStyle(_q('body')).getPropertyValue('overflow'),
			scroll: window.scrollY
		};
		App.style('html,body', {overflow: 'hidden'});
		App.hide('#gallery');

		// Pause sur les vidéos.
		App.each('video', elem => elem.pause());

		// Si le diaporama a déjà été initialisé.
		if (_init)
		{
			if (_currentPosition != position)
			{
				_deleteItems(true);
				_currentPosition = position;
				_itemsCount = 0;
				_realsize = false;
				_getData(true, false, position, position);
				App.removeClass('#diaporama_switch', 'fullsize');
			}

			_fullScreen();

			diaporama.style.display = 'flex';

			_carouselChangeSizePosition();

			_changeCenterSizePosition();

			_keyboardActive = true;

			_changeItemSizePosition(_currentPosition, false, true);

			App.on(window, 'resize', _resize);

			return;
		}

		// Insertion du code HTML.
		if (diaporama)
		{
			diaporama.remove();
		}
		_insertHTML();

		// Options.
		_setOptions(options);

		// Mode plein écran.
		_fullScreen();

		// Redimensionnement du diaporama.
		App.on(window, 'resize', _resize);

		// Position.
		_currentPosition = position;

		// Récupération des données.
		_getData(true, false, position, position);

		// Durée d'affichage des fichiers en lecture automatique.
		_autoTextDuration();

		// On affiche le diaporama.
		App.show('#diaporama', 'flex');

		// Change les dimensions et la position du carrousel.
		_carouselChangeSizePosition();

		// Change la taille des conteneurs des éléments à centrer.
		_changeCenterSizePosition();

		// Dimensions actuelles du diaporama.
		_previousDiaporamaSize = _getDiaporamaSize();

		// Contrôle au clavier.
		_keyboard();
	};



	/**
	 * Génère une alerte pour afficher un message d'erreur.
	 *
	 * @param string message
	 *
	 * @return void
	 */
	function _alertError(message)
	{
		if (message == '')
		{
			message = 'Unknown error.';
		}
		else if (message.length > 500)
		{
			message = message.slice(0, 500) + '...';
		}
		alert('Error: ' + message);
	}

	/**
	 * Change la durée d'affichage des fichiers en lecture automatique.
	 *
	 * @param float i
	 *   Intervalle entre chaque palier de durée.
	 * @param integer d
	 *   Durée entre chaque palier de durée
	 *   lorsqu'on reste appuyé sur le bouton.
	 *
	 * @return void
	 */
	function _autoChangeDuration(i, d)
	{
		// Si le bouton n'est plus appuyé, on ne va pas plus loin.
		if (!_autoDurationMouseDown)
		{
			return;
		}

		// Accélération.
		d = !d ? 170 : (d > 20 ? d - 5 : d);

		// La durée doit-être comprise entre une valeur minimale et une valeur maximale.
		if (_options.autoDuration + i >= _options.autoDurationMin
		 && _options.autoDuration + i <= _options.autoDurationMax)
		{
			_options.autoDuration = Math.round((_options.autoDuration + i) * 10) / 10;

			_autoTextDuration();

			setTimeout(() => _autoChangeDuration(i, d), d);
		}
	}

	/**
	 * Gère le changement de fichiers durant la lecture automatique.
	 *
	 * @param bool start
	 *   Démarre-t-on la lecture automatique ?
	 *
	 * @return void
	 */
	function _autoChangeItem(start)
	{
		// Si la lecture automatique a été stoppée, on casse la boucle.
		if (!_autoActive)
		{
			return;
		}

		if (!start)
		{
			// Tant que l'on est pas au dernier fichier, on continue.
			if (_currentPosition != _itemsCount)
			{
				_changeItem('next');
			}

			// Si l'on est arrivé au dernier fichier, on recommence
			// au premier si l'option pour boucler est à true.
			else if (_options.autoLoop)
			{
				_changeItem('first');
			}
		}

		// On attend le chargement du fichier avant de passer au suivant.
		const current_item = _q('#diaporama_item_' + _currentPosition);
		const event_type = _items[_currentPosition].is_video ? 'loadeddata' : 'load';
		const loading = _q('#diaporama_loading');
		const next = () =>
		{
			const transition_duration = _options.transitionEffect != 'none' && !start
				? parseInt(_options.transitionDuration)
				: 0;
			clearTimeout(event_timer);
			App.off(current_item, event_type, next);
			App.hide(loading);
			if (_autoActive)
			{
				_autoTimer = setTimeout(
					() => _autoChangeItem(),
					(_options.autoDuration * 1000) + transition_duration
				);
			}
		};
		let event_timer;
		if (current_item.complete
		 || current_item.readyState == 4
		 || current_item.readyState == 'complete')
		{
			next();
		}
		else
		{
			App.show(loading);
			App.on(current_item, event_type, next);
			event_timer = setTimeout(next, 5000);
		}
	}

	/**
	 * Gestion de l'affichage des messages utilisés pour la lecture automatique.
	 *
	 * @param string text
	 *   Texte à afficher.
	 *
	 * @return void
	 */
	function _autoChangeMessage(text)
	{
		const msg = _q('#diaporama_auto_message');
		const options = {duration: _options.animate ? 1000 : 0, fill: 'forwards'};

		function fadeout()
		{
			options.delay = _options.animate ? 2000 : 3000;
			App.animate(msg, {opacity: 0}, options, () => App.hide(msg));
		}

		App.text(msg, text);

		if (msg.checkVisibility())
		{
			msg.getAnimations().forEach(a => a.cancel());
			msg.style.opacity = 1;
			fadeout();
		}
		else
		{
			App.style(msg, {display: 'block', opacity: 0});
			App.animate(msg, {opacity: 1}, options, fadeout);
		}
	}

	/**
	 * Ajoute au diaporama la durée d'affichage
	 * des fichiers lors de la lecture automatique.
	 *
	 * @return void
	 */
	function _autoTextDuration()
	{
		let duration = _options.autoDuration.toString();

		App.val('#diaporama_auto_duration_option input', duration);

		if (!duration.match(/\./))
		{
			duration += '.0';
		}
		App.text(
			'#diaporama_auto_duration,#diaporama_auto_duration_option span',
			`${duration} s`
		);

		if (_init && !_q('#diaporama_bottom').checkVisibility())
		{
			_autoChangeMessage(duration);
		}
	}

	/**
	 * Change l'indicateur du fichier courant du caroussel.
	 *
	 * @return void
	 */
	function _carouselChangeCurrent()
	{
		if (!_options.carousel)
		{
			return;
		}

		App.removeClass('#diaporama_carousel_thumbs dl', 'current');
		App.addClass('#diaporama_carousel_image_' + _currentPosition, 'current');
	}

	/**
	 * Gestion des pages du carrousel.
	 *
	 * @return void
	 */
	function _carouselChangePages()
	{
		// Détermine le nombre de pages ainsi que la page courante.
		const i = Math.floor(_carouselMaxThumbs) - 1;
		_carouselPagesCount = Math.ceil((_itemsCount - 1) / i);
		_carouselCurrentPage = Math.ceil(_carouselCurrentPosition / i);
		if (_carouselCurrentPage > _carouselPagesCount)
		{
			_carouselCurrentPage = _carouselPagesCount
		}

		// Bouton précédent.
		App.toggleClass('a[data-name="carousel_prev"]', 'disabled',
			_carouselCurrentPage <= 1);

		// Bouton suivant.
		App.toggleClass('a[data-name="carousel_next"]', 'disabled',
			_carouselCurrentPage >= _carouselPagesCount);
	}

	/**
	 * Change les dimensions et la position du carrousel.
	 *
	 * @param int width
	 *
	 * @return void
	 */
	function _carouselChangeSizePosition(width)
	{
		const s = _getDiaporamaSize();
		s.availableWidth += width ? width : 0;
		const next_icon = _q('a[data-name="carousel_next"]');
		const prev_icon = _q('a[data-name="carousel_prev"]');
		const next_css = {left: (s.availableWidth - prev_icon.offsetWidth) + 'px'};

		function change()
		{
			// Détermine le nombre maximum de vignettes que peut contenir le carrousel.
			_carouselMaxThumbs = (s.availableWidth
				- (prev_icon.offsetWidth * 2)
				- _options.carouselThumbsMargin)
				/ (_options.carouselThumbsSize + (_options.carouselThumbsBorder * 2)
				+ _options.carouselThumbsMargin);

			// On regénère les images.
			_carouselMaxPosition = 0;
			_carouselChangeThumbs();
		}

		// Hauteur du carrousel.
		const height_add = (_options.carouselThumbsMargin * 2)
			+ (_options.carouselThumbsBorder * 2) + _options.carouselBorderTop;
		_q('#diaporama_carousel').style.height
			= (_options.carouselThumbsSize + height_add) + 'px';

		// On crée une animation seulement lorsqu'un panneau latéral est affiché ou caché.
		const options =
		{
			duration: width && _options.animate ? _options.sidebarShowDuration : 0,
			easing: 'ease-in-out',
			fill: 'forwards'
		};
		App.animate(next_icon, next_css, options, change);
	}

	/**
	 * Change les vignettes du carrousel.
	 *
	 * @param string nav
	 *
	 * @return void
	 */
	function _carouselChangeThumbs(nav)
	{
		if (_carouselThumbs === undefined)
		{
			return;
		}

		let margin_left = 0;

		// Retourne la position à partir de laquelle on
		// doit afficher les vignettes dans le carrousel.
		function get_carousel_start()
		{
			const i = Math.floor(_carouselMaxThumbs) - 1;

			return ((Math.ceil(_carouselCurrentPosition / i) - 1) * i) + 1;
		}

		if (_currentPosition >= _carouselMaxPosition)
		{
			const max_thumbs = Math.ceil(_carouselMaxThumbs);
			const thumbs_count = Math.floor(_carouselMaxThumbs) - 1;
			const thumbs = _q('#diaporama_carousel_thumbs');

			// Position à partir de laquelle on affiche les vignettes.
			let start = get_carousel_start();
			if (start == _itemsCount)
			{
				start -= thumbs_count;
				if (start < 1)
				{
					start = 1;
				}
			}

			let dl = [..._qAll(thumbs, 'dl')];

			// Dans le cas d'une navigation dans le carrousel,
			// on supprimera les vignettes après l'animation.
			if (nav)
			{
				_carouselAnimation = true;
				_carouselCurrentPosition = start;

				margin_left = thumbs_count * (_options.carouselThumbsSize
					+ (_options.carouselThumbsBorder * 2) + _options.carouselThumbsMargin);

				dl = nav == 'prev'
					? dl.slice(max_thumbs - thumbs_count + 1)
					: dl.slice(0, thumbs_count);
				dl.forEach(elem => elem.classList.add('remove'));
			}

			// Sinon, on supprime tout de suite toutes les vignettes,
			// mais seulement si c'est nécessaire.
			else
			{
				const first_position = dl.length
					? parseInt(_q(thumbs, 'dl:first-child')
						.id.replace(/diaporama_carousel_image_(\d+)/, '$1'))
					: start;
				if (first_position != start)
				{
					App.empty(thumbs);
				}
			}

			// On génère le code HTML des nouvelles vignettes.
			let html = '', n = 0;
			for (const pos in _carouselThumbs)
			{
				if (pos < start || _q('#diaporama_carousel_image_' + pos))
				{
					continue;
				}
				if (nav == 'prev' && n == thumbs_count)
				{
					break;
				}
				n++;

				const duration = _carouselThumbs[pos].is_video
					? '<span class="duration"></span>'
					: '';
				html +=
					`<dl id="diaporama_carousel_image_${pos}">` +
						`<dt><a rel="${n}">${duration}<img></a></dt>` +
					`</dl>`;

				_carouselMaxPosition = pos;

				if (n == max_thumbs)
				{
					break;
				}
			}
			if (nav == 'prev')
			{
				thumbs.style.marginLeft = -margin_left + 'px';
				App.prepend(thumbs, html);
			}
			else if (html !== '')
			{
				App.append(thumbs, html);
			}

			// Ajout des données.
			const size = parseInt(_options.carouselThumbsSize) + 'px';
			for (const pos in _carouselThumbs)
			{
				const id = _q('#diaporama_carousel_image_' + pos);
				if (!id)
				{
					continue;
				}

				const i = _carouselThumbs[pos];
				const a = _q(id, 'a');
				const span = _q(a, 'span');

				App.style(a, {width: size, height: size});
				App.attr(id, 'title', i.title);
				App.attr([a, 'img'], 'src', i.source);
				if (span)
				{
					App.text(span, i.duration);
				}
			}

			// Suppression des anciennes vignettes lors
			// de la navigation dans le carrousel.
			if (nav)
			{
				const nav_css = {marginLeft: nav == 'prev' ? 0 : -margin_left + 'px'};
				const options =
				{
					duration: _options.animate ? _options.carouselNavDuration : 0,
					easing: 'ease-in-out'
				};
				App.animate(thumbs, nav_css, options, () =>
				{
					App.remove([thumbs, 'dl.remove']);
					thumbs.style.marginLeft = 0;
					_getData(false, false, 0, _carouselCurrentPosition);
					_carouselAnimation = false;
				});
			}

			// Gestion de l'événement "click" sur les vignettes du carrousel.
			App.click([thumbs, 'dl'], function()
			{
				const items_count = _itemsCount;
				const new_current_position = parseInt(
					this.id.replace(/diaporama_carousel_image_(\d+)/, '$1')
				);

				if (new_current_position == _currentPosition)
				{
					return;
				}

				// On réinitialise le diaporama et on récupère les informations des images.
				_options.autoStart = false;
				_zoom = {};
				_reload(new_current_position);

				// Position du fichier courant dans le carrousel.
				_carouselChangeCurrent();

				// On regénère le carrousel.
				if (parseInt(_q(this, 'a').rel) >= Math.floor(_carouselMaxThumbs))
				{
					_carouselMaxPosition = 0;
					_getData(false, false, 0, new_current_position);
				}

				// On réinitialise la lecture automatique.
				if (_autoActive)
				{
					if ((new_current_position == items_count) && !_options.autoLoop)
					{
						_icon('auto').click();
					}
					else
					{
						clearTimeout(_autoTimer);
						_autoTimer = setTimeout(_autoChangeItem, _options.autoDuration * 1000);
					}
				}
			});

			// Change les pages du carrousel.
			_carouselChangePages();
		}

		// Position du fichier courant dans le carrousel.
		if (!nav)
		{
			_carouselChangeCurrent();
		}
	}

	/**
	 * Modifie les boutons de navigation.
	 *
	 * @return void
	 */
	function _changeButtonsNavigation()
	{
		const buttons =
		{
			first: 1,
			prev: _currentPosition <= 2 ? 1 : _currentPosition - 1,
			next: _currentPosition >= _itemsCount ? _itemsCount : _currentPosition + 1,
			last: _itemsCount
		};

		for (const button in buttons)
		{
			App.toggleClass(`a[data-name="${button}"]`, 'disabled',
				!_items[buttons[button]] || _currentPosition == buttons[button]);
		}
	}

	/**
	 * Change le bouton du switch taille réelle / taille redimensionnée du fichier.
	 *
	 * @param int width
	 * @param int height
	 *
	 * @return void
	 */
	function _changeButtonSwitch(width, height)
	{
		if (_animationActive)
		{
			return;
		}

		if (_items[_currentPosition] === undefined || _switchActive)
		{
			_changeCursor('default');
			return;
		}

		const s = _getDiaporamaSize();
		const item = _q('#diaporama_item_' + _currentPosition);
		const diapo_switch = _q('#diaporama_switch');

		if (!item)
		{
			_changeCursor('default');
			return;
		}

		const is_video = _items[_currentPosition].is_video;
		const item_width = width || item.offsetWidth;
		const item_height = height || item.offsetHeight;
		const exceeds = (s.availableWidth > 0 && item_width > s.availableWidth)
					 || (s.availableHeight > 0 && item_height > s.availableHeight);
		const zoom = item_width > _items[_currentPosition].width_resized;
		const zoom_disabled = item_width == _items[_currentPosition].width_resized;

		App.removeClass(diapo_switch, 'disabled', 'fullsize');

		// Bouton de switch.
		if (zoom || exceeds)
		{
			App.addClass(diapo_switch, 'fullsize');
		}
		else if (zoom_disabled)
		{
			App.addClass(diapo_switch, 'disabled');

			if (_zoom.width == item_width)
			{
				_realsize = false;
				_zoom = {};
			}
		}

		if (_zoom.width != item_width)
		{
			_zoom = {};
		}

		// On indique la nouvelle taille en pourcentage.
		_changePosition(item_width);

		// Pointeur.
		_changeCursor(exceeds && !is_video
			? (_options.clickZoom
				? (item_width >= _items[_currentPosition].width_resized ? 'zoom-out' : 'zoom-in')
				: 'move')
			: (_swipeActive
				? 'grab'
				: (!_options.clickZoom || zoom_disabled || is_video ? 'default' : 'zoom-in')));

		// Déplacement de l'image.
		App.off(item, {'mousedown': _dragImageMousedown, 'touchstart': _dragImageTouchstart});
		if (exceeds && !is_video)
		{
			App.on(item, {'mousedown': _dragImageMousedown, 'touchstart': _dragImageTouchstart});
		}
	}

	/**
	 * Change la taille des conteneurs des éléments à centrer.
	 *
	 * @param int width
	 *
	 * @return void
	 */
	function _changeCenterSizePosition(width)
	{
		const s = _getDiaporamaSize();
		s.availableWidth += width ? width : 0;
		s.availableWidth = s.availableWidth < 0 ? 0 : s.availableWidth;

		const margin_left = width ? (width < 0 ? width : 0) : -s.sidebarWidth;

		const css =
		{
			inner:
			{
				width: s.availableWidth + 'px'
			},
			keyboard:
			{
				maxWidth: s.availableWidth + 'px',
				marginLeft: margin_left + 'px',
				height: s.availableHeight + 'px',
				top: s.barTopHeight + 'px'
			},
			loading:
			{
				top: s.barTopHeight + 'px',
				width: s.availableWidth + 'px',
				height: s.availableHeight + 'px'
			},
			text:
			{
				bottom: (s.barBottomHeight + s.carouselHeight) + 'px',
				marginLeft: margin_left + 'px',
				maxWidth: s.availableWidth + 'px'
			}
		};

		const options =
		{
			duration: !width || !_options.animate ? 0 : _options.sidebarShowDuration,
			easing: 'ease-in-out',
			fill: 'forwards'
		};
		for (const [name, prop] of Object.entries(css))
		{
			App.animate('#diaporama_' + name, prop, options);
		}
	}

	/**
	 * Change le pointeur de la souris sur le fichier courant et la zone d'affichage.
	 *
	 * @param string cursor
	 *
	 * @return void
	 */
	function _changeCursor(cursor)
	{
		App.style('#diaporama_inner,#diaporama_item_' + _currentPosition, {cursor: cursor});
	}

	/**
	 * Change le fichier en fonction du bouton de navigation cliqué.
	 *
	 * @param object button
	 *   Bouton de navigation qui a été cliqué.
	 * @param bool click
	 *   Indique si la fonction a été appelée par un click.
	 *
	 * @return void
	 */
	function _changeItem(button, click)
	{
		// On détermine la position du fichier à afficher.
		let new_current_position;
		switch (button)
		{
			case 'first' :
				new_current_position = 1;
				break;

			case 'prev' :
				new_current_position = _currentPosition - 1;
				break;

			case 'next' :
				new_current_position = _currentPosition + 1;
				break;

			case 'last' :
				new_current_position = _itemsCount;
				break;
		}

		// Si le fichier n'existe pas
		// ou correspond à une valeur impossible
		// on bien si un effet de transition entre fichiers est en cours,
		// on ne va pas plus loin.
		if (!_q('#diaporama_item_' + new_current_position)
		|| new_current_position < 1 || new_current_position > _itemsCount || _animationActive)
		{
			return;
		}

		// On met les vidéos sur pause.
		App.each('#diaporama video', elem => elem.pause());

		// Permet d'éviter un bug d'affichage avec certains navigateurs.
		if (!navigator.userAgent.includes('Firefox')
		&& App.hasClass('#diaporama_switch', 'fullsize')
		&& [..._qAll('.diaporama_sidebar')].find(e => e.checkVisibility()))
		{
			const animate = _options.animate;
			_options.animate = false;
			_q('#diaporama_switch').click();
			_options.animate = animate;
		}

		// On redimensionne le fichier à afficher.
		_realsize = false;
		_zoom = {};
		App.removeClass('#diaporama_switch', 'fullsize');

		// On modifie la valeur de la position courante.
		const old_current_position = _currentPosition;
		_currentPosition = new_current_position;

		// On indique les bonnes dimensions et position au fichier que l'on va afficher.
		_changeItemSizePosition(new_current_position);

		// Transition entre le fichier courant et le fichier demandé.
		_itemTransition(old_current_position, new_current_position, button);

		// Change les boutons de navigation.
		_changeButtonsNavigation();

		// En lecture automatique, si l'on est arrivé au dernier
		// fichier et que l'option pour boucler est désactivée,
		// on stoppe la lecture automatique.
		if (_autoActive && new_current_position == _itemsCount && !_options.autoLoop)
		{
			_icon('auto').click();
		}

		// Si le changement de fichier s'est fait manuellement,
		// et que la lecture automatique est en cours,
		// alors on réinitialise la lecture automatique.
		if (click && typeof _autoTimer == 'number')
		{
			// On stoppe la lecture automatique.
			clearTimeout(_autoTimer);
			_autoTimer = undefined;

			// On redémarre la lecture automatique.
			_autoChangeItem(true);
		}
	}

	/**
	 * Change les informations du fichier courant affichées dans le diaporama.
	 *
	 * @return void
	 */
	function _changeItemInfos()
	{
		if (_items[_currentPosition] === undefined)
		{
			return;
		}

		const current = _items[_currentPosition];
		const current_item = _q('#diaporama_item_' + _currentPosition);

		// Identifiant du fichier.
		DIAPORAMA.item_id = current.id;

		// Fil d'Ariane.
		const bc_parents = _q('#diaporama_breadcrumb_parents');
		const bc_item = _q('#diaporama_breadcrumb_item');
		App.empty(bc_parents);
		App.empty(bc_item);
		if (typeof current.breadcrumb == 'object')
		{
			for (const i in current.breadcrumb)
			{
				const a = document.createElement('a');
				App.text(a, current.breadcrumb[i].name);
				App.attr(a, 'href', current.breadcrumb[i].url);
				App.append(bc_parents, '<span>/</span>');
				if (parseInt(i) == Object.keys(current.breadcrumb).length)
				{
					bc_item.append(a);
				}
				else
				{
					bc_parents.append(a);
				}
			}
		}

		// Bouton de téléchargement.
		if (current.download)
		{
			const download_icon = _icon('download');
			App.attr(download_icon,
			{
				'title': _l10n.download + ` (${current.stats.filesize.text})`,
				'href': current.download
			});
			App.show(download_icon, 'inline');
			App.off(current_item, 'contextmenu', evt => evt.preventDefault());
		}
		else
		{
			App.hide(_icon('download'));
			App.on(current_item, 'contextmenu', evt => evt.preventDefault());
		}

		// Note utilisateur.
		if (_params.votes)
		{
			const user_rating = _q('#diaporama_user_note_rating');
			for (const i in current.user.rating_array)
			{
				App.html(
					`#diaporama_user_note_rating a[data-rating="${parseInt(i) + 1}"]`,
					current.user.rating_array[i] ? '&#xe9d9;' : '&#xe9d7;'
				);
			}
			_q('#diaporama_user_note_delete').style.visibility
				= current.user.rating ? 'visible' : 'hidden';
			_q('#diaporama_user_note').style.display = current.votable ? 'block' : 'none';
			_q('#diaporama_votes_disabled').style.display = current.votable ? 'none' : 'block';
		}

		// Édition.
		if (current.user.edit && window.innerWidth > _options.mobileMaxWidth)
		{
			const tags = current.tags.map(tag => { return tag.name; });
			App.val('#diaporama_edit_title', current.title);
			App.val('#diaporama_edit_filename', current.filename);
			App.val('#diaporama_edit_desc', current.description);
			App.val('#diaporama_edit_tags', tags.join(', '));
		}

		// Géolocalisation.
		if (App.hasClass(_icon('geolocation'), 'active'))
		{
			_geolocation();
		}

		// Icônes.
		_sidebarsIcons();

		// Informations : création des différentes parties.
		const ul_infos = _q(
			'.diaporama_sidebar[data-name="informations"] .diaporama_sidebar_inner > ul'
		);
		App.empty(ul_infos);
		for (const name of ['user', 'desc', 'stats', 'properties', 'tags', 'exif', 'iptc', 'xmp'])
		{
			const li =
				`<li id="diaporama_sidebar_${name}" data-info-name="${name.charAt(0)}">` +
					`<h2><span></span></h2>` +
					`<div class="diaporama_sidebar_content"></div>` +
				`</li>`;
			App.append(ul_infos, li);
			App.text(`#diaporama_sidebar_${name} h2 span`, _l10n[name]);
		}

		// Informations : gestion des parties pliables.
		App.click('.diaporama_sidebar[data-name="informations"] h2 span', function()
		{
			const li = this.closest('li');
			const name = App.attr(li, 'data-info-name');
			const content = _q(li, '.diaporama_sidebar_content');

			if (_options.sidebarInformations[name] == 1)
			{
				App.hide(content);
				_options.sidebarInformations[name] = 0;
			}
			else
			{
				App.show(content, 0, name == 'u' ? 'flex' : 'block');
				_options.sidebarInformations[name] = 1;
			}
			_savePrefs();
		});

		// Informations : titre et description.
		const desc = _q('#diaporama_sidebar_desc');
		if (current.description_formated)
		{
			App.html([desc, '.diaporama_sidebar_content'], current.description_formated);
			App.show(desc);
		}
		else
		{
			App.hide(desc);
		}
		_changeOverImageTextDisplay();

		// Informations : utilisateur.
		const user = _q('#diaporama_sidebar_user');
		if (current.owner)
		{
			const user_content = _q(user, '.diaporama_sidebar_content');
			App.html(user_content, '<span id="diaporama_user_avatar"></span><p></p>');

			// Avatar.
			const img_html = '<img width="50" height="50">';
			if (current.owner.link)
			{
				App.prepend([user_content, 'span'], '<a tabindex="-1"></a>');
				const a = '#diaporama_user_avatar a';
				App.attr(a, 'href', current.owner.link);
				App.prepend(a, img_html);
			}
			else
			{
				App.prepend('#diaporama_user_avatar', img_html);
			}
			const img = _q('#diaporama_user_avatar img');
			App.attr(img, {'alt': _l10n.avatar, 'src': current.owner.avatar_source});

			// Nom d'utilisateur et date d'ajout.
			const p = _q(user_content, 'p');
			const pub = _l10n.date_user_published.split('%s');
			[pub[0], document.createElement('a'), pub[1], document.createElement('span'), pub[2]]
			.forEach(e => p.append(e));
			App.attr([p, 'span'], 'id', 'diaporama_user_name');

			// Date d'ajout.
			App.text([p, 'a'], current.date_published_text);
			App.attr([p, 'a'], 'href', current.date_published_link);

			// Nom d'utilisateur.
			let username = _q('#diaporama_user_name');
			if (current.owner.link)
			{
				App.prepend(username, '<a></a>');
				username = _q(username, 'a');
				App.attr(username, 'href', current.owner.link);
			}
			App.text(username, current.owner.nickname);
		}
		else
		{
			App.hide(user);
		}

		// Informations : statistiques et propriétés.
		const lists =
		{
			stats: ['views', 'favorites', 'comments', 'votes', 'rating'],
			properties: ['filetype', 'filesize', 'size', 'duration', 'date_created']
		};
		for (const list in lists)
		{
			const content = _q(`#diaporama_sidebar_${list} .diaporama_sidebar_content`);
			App.prepend(content, '<ul></ul>');
			for (const name of lists[list])
			{
				App.append([content, 'ul'],
					`<li id="diaporama_sidebar_${list}_${name}"> <span></span></li>`
				);
				App.prependText(`#diaporama_sidebar_${list}_${name}`, _l10n[list + '_' + name]);
			}
		}

		// Informations : statistiques.
		for (const stat in current.stats)
		{
			const stat_li = _q('#diaporama_sidebar_stats_' + stat);
			if (!stat_li)
			{
				continue;
			}
			if (Object.keys(current.stats[stat]).length)
			{
				if (stat == 'rating')
				{
					let entity, html = '';
					for (const key in current.stats[stat].array)
					{
						if (current.stats[stat].array[key] == 1)
						{
							entity = '&#xe9d9;';
						}
						else if (current.stats[stat].array[key] == 0)
						{
							entity = '&#xe9d7;';
						}
						else
						{
							entity = '&#xe9d8;';
						}
						html += `<span class="rating">${entity}</span>`;
					}
					App.html([stat_li, 'span'], html);
					App.attr([stat_li, 'span'], 'title', current.stats[stat].formated);

					// Panneau d'ajout d'une note.
					const note = _q('#diaporama_note_rating');
					const votes_l10n = current.stats.votes.short > 1
						? _l10n.votes_number_multiple
						: _l10n.votes_number;
					App.html(note, html);
					App.attr('#diaporama_note_rating', 'title', current.stats[stat].formated);
					App.text('#diaporama_note_formated',
						votes_l10n.replace('%s', current.stats.votes.short));
				}
				else
				{
					App.text([stat_li, 'span'], current.stats[stat].short);
				}
				App.show(stat_li);
			}
			else
			{
				App.hide(stat_li);
			}
		}

		// Informations : propriétés.
		const prop = '#diaporama_sidebar_properties_';
		App.text(prop + 'filetype span', current.type_text);
		App.text(prop + 'filesize span', current.stats.filesize.text);
		App.text(prop + 'size span:first-child',
			current.width + ' x ' + current.height + ' ' + _l10n['pixels']);

		const duration = _q(prop + 'duration');
		if (current.is_video)
		{
			App.show(duration);
			App.text([duration, 'span'], current.duration_text);
		}
		else
		{
			App.hide(duration);
		}

		const date_created = _q(prop + 'date_created');
		if (current.date_created_text)
		{
			const span = _q(date_created, 'span');
			const a = document.createElement('a');
			App.attr(a, 'href', current.date_created_link);
			App.text(a, current.date_created_text);
			App.html(span, a);
			App.show(date_created);
		}
		else
		{
			App.hide(date_created);
		}

		// Informations : tags.
		_tags(current);

		// Informations : métadonnées.
		['exif', 'iptc', 'xmp'].forEach(meta =>
		{
			const element = _q('#diaporama_sidebar_' + meta);
			if (Object.keys(current[meta]).length)
			{
				App.show(element);

				const meta_content = _q(element, '.diaporama_sidebar_content');
				App.html(meta_content, document.createElement('ul'));

				for (const data in current[meta])
				{
					const li = document.createElement('li');
					App.text(li, `${current[meta][data].name} : `);
					const span = document.createElement('span');
					if (current[meta][data].link)
					{
						const a = document.createElement('a');
						App.attr(a, 'href', current[meta][data].link);
						App.text(a, current[meta][data].value);
						span.append(a);
					}
					else
					{
						App.text(span, current[meta][data].value);
					}
					li.append(span);
					_q(meta_content, 'ul').append(li);
				}
			}
			else
			{
				App.hide(element);
			}
		});

		// Dans les favoris ?
		const fav_icon = _icon('favorite');
		if (fav_icon)
		{
			const infav = current.user.in_favorites;
			App.attr(fav_icon, 'title', _l10n[`favorites_${infav ? 'remove' : 'add'}`]);
			App.toggleClass(fav_icon, 'active', infav);
			App.toggleClass('#diaporama_favorite', 'in', infav);
		}

		// Dans la sélection ?
		const sel_icon = _icon('selection');
		if (sel_icon)
		{
			const insel = current.in_selection;
			App.attr(sel_icon, 'title', _l10n[`selection_${insel ? 'remove' : 'add'}`]);
			App.toggleClass(sel_icon, 'active', insel);
			App.toggleClass('#diaporama_selection', 'in', insel);
		}

		// Parties pliables du panneau des informations.
		if (typeof _options.sidebarInformations == 'object')
		{
			for (const name in _options.sidebarInformations)
			{
				App.style(`li[data-info-name="${name}"] .diaporama_sidebar_content`,
				{
					display: _options.sidebarInformations[name] == 1
						? (name == 'u' ? 'flex' : 'block')
						: 'none'
				});
			}
		}

		// Carrousel.
		_carouselChangeCurrent();

		// Change le bouton de switch.
		_changeButtonSwitch();

		// Position dans la série.
		_changePosition();
	}

	/**
	 * Change les dimensions et la position (coordonnées spatiales) du fichier
	 * en fonction de l'espace disponible dans la zone d'affichage.
	 *
	 * @param int position
	 *   Position du fichier dans la série (entre 1 et itemsCount).
	 *   A ne pas confondre avec la position (top, left) du fichier
	 *   dans la zone d'affichage.
	 * @param bool animate
	 *   Doit-on autoriser l'animation du fichier ?
	 * @param bool visible
	 *   Doit-on afficher le fichier ?
	 * @param bool return_css
	 *   Doit-on retourner les règles CSS au lieu de les appliquer au fichier ?
	 * @param int add_width
	 *   Largeur à ajouter à la largeur de la zone d'affichage.
	 * @param function callback
	 *
	 * @return mixed
	 *   Retourne les valeurs CSS, si demandées.
	 */
	function _changeItemSizePosition(position, animate,
	visible, return_css, add_width = 0, callback)
	{
		const item = _q('#diaporama_item_' + position);

		if (_items[position] === undefined || !item)
		{
			return;
		}

		// Paramètres du diaporama.
		const s = _getDiaporamaSize();
		s.availableWidth += add_width;

		// Paramètres du fichier.
		const item_width = _items[position].width_resized;
		const item_height = _items[position].height_resized;
		const width_ratio = item_width / s.availableWidth;
		const height_ratio = item_height / s.availableHeight;

		let item_contain_width = item_width;
		let item_contain_height = item_height;
		let item_width_resize = item_width;
		let item_height_resize = item_height;

		// Dimensions (redimensionnées) selon l'espace disponible.
		if ((item_width > s.availableWidth) && (width_ratio >= height_ratio))
		{
			item_contain_width = Math.round(s.availableWidth);
			item_contain_height = Math.round(item_height / width_ratio);
		}
		if ((item_height > s.availableHeight) && (height_ratio >= width_ratio))
		{
			item_contain_width = Math.round(item_width / height_ratio);
			item_contain_height = Math.round(s.availableHeight);
		}
		App.attr(item,
		{
			'data-contain-width': item_contain_width,
			'data-contain-height': item_contain_height
		});

		if (!_realsize)
		{
			item_height_resize = item_contain_height;
			item_width_resize = item_contain_width;
		}

		if (_zoom.width)
		{
			if (_zoom.width < item_contain_width)
			{
				item_height_resize = item_contain_height;
				item_width_resize = item_contain_width;
			}
			else
			{
				item_height_resize = _zoom.height;
				item_width_resize = _zoom.width;
			}
		}

		// Position du fichier.
		let item_offset = {top: item.offsetTop, left: item.offsetLeft};

		// Position du fichier, largeur : modification de la taille du fichier.
		if (item_width_resize <= s.availableWidth || item_width_resize != item.offsetWidth)
		{
			item_offset.left = (s.availableWidth - item_width_resize) / 2;
		}

		// Position du fichier, largeur : modification de la taille de la zone d'affichage.
		else if (item_width_resize > s.availableWidth
		&& s.availableWidth != _previousDiaporamaSize.availableWidth)
		{
			item_offset.left += (s.availableWidth - _previousDiaporamaSize.availableWidth) / 2;

			const right = item_width_resize - s.availableWidth + item_offset.left;
			if (right < 0)
			{
				item_offset.left -= right;
			}
			if (item_offset.left > 0)
			{
				item_offset.left = 0;
			}
		}

		// Position du fichier, hauteur : modification de la taille du fichier.
		if (item_height_resize <= s.availableHeight || item_height_resize != item.offsetHeight)
		{
			item_offset.top = ((s.availableHeight - item_height_resize) / 2) + s.barTopHeight;
		}

		// Position du fichier, hauteur : modification de la taille de la zone d'affichage.
		else if (item_height_resize > s.availableHeight
		&& s.availableHeight != _previousDiaporamaSize.availableHeight)
		{
			item_offset.top += ((s.availableHeight - _previousDiaporamaSize.availableHeight) / 2);
			const bottom = item_height_resize
				- (s.availableHeight + s.barBottomHeight + s.carouselHeight)
				+ item_offset.top;
			if (bottom < 0)
			{
				item_offset.top -= bottom;
			}
			if (item_offset.top > s.barTopHeight)
			{
				item_offset.top = s.barTopHeight;
			}
		}

		// Valeurs CSS.
		const new_width = Math.round(item_width_resize);
		const new_height = Math.round(item_height_resize);
		const css =
		{
			top: Math.round(item_offset.top) + 'px',
			left: Math.round(item_offset.left) + 'px',
			width: new_width + 'px',
			height: new_height + 'px'
		};

		// On applique les nouvelles propriétés au fichier.
		if (!return_css)
		{
			function finish()
			{
				Object.assign(item.style, css);
				if (callback)
				{
					callback();
				}
			}
			if (animate && item.checkVisibility() && _options.animate)
			{
				_animationActive = true;
				App.animate(item, css,
				{
					duration: _options.itemResizeDuration,
					easing: 'ease-in-out'
				},
				() =>
				{
					_animationActive = false;
					finish();
				});
			}
			else
			{
				finish();
			}
			App.style(
				'#diaporama_favorite,#diaporama_selection',
				{left: s.availableWidth + 'px'}
			);
		}

		// Doit-on afficher le fichier ?
		if (visible)
		{
			App.show(item);
		}

		// Doit-on retourner les valeurs CSS ?
		if (return_css)
		{
			return css;
		}

		// Pour le fichier courant.
		if (position == _currentPosition)
		{
			_changeButtonSwitch(new_width, new_height);
		}
	}

	/**
	 * Change le contenu et l'affichage du titre
	 * et de la description par dessus les images.
	 *
	 * @return void
	 */
	function _changeOverImageTextDisplay()
	{
		const current = _items[_currentPosition];

		// Description.
		const desc = _q('#diaporama_description');
		const desc_condition = _options.overImageDescription && current.description_formated;
		App.html(desc, current.description_formated);
		desc.scrollTop = 0;
		desc.style.display = desc_condition ? 'block' : 'none';

		// Titre.
		const title = _q('#diaporama_title');
		App.text(title, current.title);
		title.scrollTop = 0;
		title.style.display = _options.overImageTitle ? 'block' : 'none';

		// Zone de texte.
		_q('#diaporama_text').style.display = !current.is_video
			&& (desc_condition || _options.overImageTitle) ? 'block' : 'none';
	}

	/**
	 * Change le texte pour la position dans la série.
	 *
	 * @param int width
	 *   Largeur du fichier.
	 *
	 * @return void
	 */
	function _changePosition(width)
	{
		if (_items[_currentPosition] === undefined || _switchActive)
		{
			return;
		}

		const current = _items[_currentPosition];
		const item = _q('#diaporama_item_' + _currentPosition);
		const pc = Math.round(((width || item.offsetWidth) / current.width_resized) * 100);

		App.text('#diaporama_position span', window.innerWidth > 700
			? `${current.position_text} - ${pc}%`
			: current.position);
	}

	/**
	 * Gestion du zoom lors du click sur une image.
	 *
	 * @param object evt
	 *
	 * @return void
	 */
	function _clickZoom(evt)
	{
		if (App.hasClass('#diaporama_switch', 'disabled'))
		{
			return;
		}

		if (evt.button === 0 || evt.button === undefined)
		{
			_clickStart = false;
			if (_clickMove)
			{
				_clickMove = false;
				return;
			}

			_zoom = {};

			// Pas de zoom si une animation est en cours, ni pour les vidéos.
			if (_animationActive || _items[_currentPosition].is_video)
			{
				return;
			}

			// Application du zoom.
			_zoomItem(evt.changedTouches ? evt.changedTouches[0] : evt, true, 100);
		}
	}

	/**
	 * Création de l'élément "<img>" ou "<video>" du fichier correspondant à "position".
	 *
	 * @param string position
	 *   Position du fichier dans la série.
	 *
	 * @return void
	 */
	function _createItem(position)
	{
		const i = _items[position];

		if (i === undefined || _q('#diaporama_item_' + position))
		{
			return;
		}

		const item = _q('#diaporama_item');
		const file = document.createElement(i.is_video ? 'video' : 'img');
		if (i.is_video)
		{
			const source = document.createElement('source');
			App.attr(source, {'src': i.source, 'type': i.type_mime});
			if (_params.video_loop)
			{
				App.attr(file, 'loop', '');
			}
			if (_params.video_loop)
			{
				App.attr(file, 'muted', '');
			}
			App.attr(file, {'controls': '', 'poster': i.poster});
			file.volume = 0.5;
			App.on(file,
			{
				'focus': () => _keyboardActive = false,
				'blur': () => _keyboardActive = true
			});
			file.append(source);
		}
		else
		{
			App.attr(file, {'src': i.source, 'data-type': i.type_mime});
		}
		App.attr(file, 'id', `diaporama_item_${position}`);
		App.addClass(file, 'diaporama_item');
		App.hide(file);
		item.append(file);

		// Icône de chargement.
		if (position == _currentPosition)
		{
			App.hide('#diaporama_loading');
		}
	}

	/**
	 * Supprime les fichiers inutiles pour ne pas encombrer et ralentir le diaporama.
	 *
	 * @param bool all
	 *   Doit-on supprimer touts les fichiers ?
	 *
	 * @return void
	 */
	function _deleteItems(all)
	{
		App.each('.diaporama_item', elem =>
		{
			const position = elem.id.replace(/diaporama_item_/, '');
			if (_items[position] === undefined || all)
			{
				elem.remove();
			}
		});
	}

	/**
	 * Gestion du déplacement de l'image.
	 *
	 * @param object img
	 * @param object evt
	 * @param bool touch
	 *
	 * @return void
	 */
	function _dragImage(img, evt, touch = false)
	{
		if (_animationActive || _swipeActive || _switchActive)
		{
			return;
		}

		const move_event = touch ? 'touchmove' : 'mousemove';
		const end_event = touch ? 'touchend' : 'mouseup';
		const s = _getDiaporamaSize();

		let x = img.offsetLeft;
		let y = img.offsetTop;
		let mx = touch ? evt.changedTouches[0].pageX : evt.pageX;
		let my = touch ? evt.changedTouches[0].pageY : evt.pageY;

		function move(e)
		{
			const newmx = touch ? e.changedTouches[0].pageX : e.pageX;
			const newmy = touch ? e.changedTouches[0].pageY : e.pageY;

			x += newmx - mx;
			y += newmy - my;
			mx = newmx;
			my = newmy;

			if (img.width > s.availableWidth)
			{
				if (x > 0)
				{
					x = 0;
				}
				else if (x < (s.availableWidth - img.width))
				{
					x = s.availableWidth - img.width;
				}
			}
			else
			{
				x = img.offsetLeft;
			}

			if (img.height > s.availableHeight)
			{
				if (y > s.barTopHeight)
				{
					y = s.barTopHeight;
				}
				else if (y < (s.barTopHeight + s.availableHeight - img.height))
				{
					y = s.barTopHeight + s.availableHeight - img.height;
				}
			}
			else
			{
				y = img.offsetTop;
			}

			img.style.left = x + 'px';
			img.style.top = y + 'px';

			_changeCursor('move');
		}

		function end()
		{
			App.off(document, move_event, move);
			App.off(document, end_event, end);
			if (!App.hasClass('#diaporama_switch', 'fullsize'))
			{
				App.off('#diaporama_item_' + _currentPosition,
				{
					'mousedown': _dragImageMousedown,
					'touchstart': _dragImageTouchstart
				});
			}

			if (_options.clickZoom)
			{
				_changeCursor(
					App.hasClass('#diaporama_switch', 'disabled')
						? 'default'
						: (_q('#diaporama_item_' + _currentPosition).offsetWidth >=
						  _items[_currentPosition].width_resized
							? 'zoom-out' : 'zoom-in')
				);
			}
		}

		App.on(document, move_event, move);
		App.on(document, end_event, end);
	}
	function _dragImageMousedown()
	{
		if (event.button === 0)
		{
			_dragImage(this, event);
		}
	}
	function _dragImageTouchstart()
	{
		_dragImage(this, event, true);
	}

	/**
	 * Affichage en mode plein écran.
	 *
	 * @return void
	 */
	function _fullScreen()
	{
		if (_options.fullScreen)
		{
			document.body.requestFullscreen?.();
		}
	}

	/**
	 * Sortie du mode plein écran.
	 *
	 * @return void
	 */
	function _fullScreenExit()
	{
		if (document.fullscreenElement)
		{
			document.exitFullscreen();
		}
	}

	/**
	 * Gestion de la géolocalisation.
	 *
	 * @param bool reset
	 *
	 * @return void
	 */
	function _geolocation(reset = false)
	{
		const data = _items[_currentPosition];
		const is_coords = data.geolocation_lat !== null && data.geolocation_long != null;

		// Chargement des fichiers.
		function load_file(url, integrity, type, id)
		{
			const elem = document.createElement(type);
			App.attr(elem, {'id': id, 'crossorigin': '', 'integrity': integrity});
			App.attr(elem, type == 'link'
				? {'rel': 'stylesheet', 'href': url}
				: {'type': 'text/javascript', 'src': url});
			App.append('head', elem);
		}

		// Aucune donnée de géolocalisation.
		function hide_map()
		{
			App.hide('#diaporama_geolocation_map,#diaporama_geolocation_coords');
			App.show('#diaporama_geolocation_none');
		}

		// Création de la carte.
		function set_map()
		{
			if (typeof L != 'object')
			{
				return;
			}

			show_map();
			_geolocationMap = L.map('diaporama_geolocation_map',
			{
				center: [data.geolocation_lat, data.geolocation_long],
				zoom: _params.geolocation_zoom
			});
			const url = _q('html').lang == 'fr'
				? 'https://{s}.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png'
				: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
			const osm = L.tileLayer(url,
			{
				attribution: '<a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
				name: 'map'
			});
			const esri = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/'
				+ 'services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
			{
				attribution: '<a href="https://www.esri.com/">Esri</a>',
				name: 'satellite'
			});

			if (_params.geolocation_layer == 'satellite')
			{
				esri.addTo(_geolocationMap);
			}
			else
			{
				osm.addTo(_geolocationMap);
			}
			const layers = {};
			layers[_l10n.geolocation_map] = osm;
			layers[_l10n.geolocation_satellite] = esri;
			L.control.layers(layers, {}, {position: 'bottomleft'}).addTo(_geolocationMap);

			_geolocationMap.on('baselayerchange', evt =>
			{
				_params.geolocation_layer = evt.layer.options.name;
			});
			_geolocationMap.on('zoom', () =>
			{
				_params.geolocation_zoom = _geolocationMap.getZoom();
			});

			set_marker();
		}

		// Création du marqueur.
		function set_marker()
		{
			if (_geolocationMarker)
			{
				_geolocationMarker.remove();
			}
			const coords = [data.geolocation_lat, data.geolocation_long];
			const marker = L.icon(
			{
				iconUrl: GALLERY.path + '/images/markers/marker-item.png',
				iconSize: [20, 34],
				iconAnchor: [10, 34],
				popupAnchor: [0, -39],
				shadowUrl: GALLERY.path + '/images/markers/marker-shadow.png',
				shadowSize: [54, 34],
				shadowAnchor: [27, 34]
			});
			_geolocationMarker = L.marker(coords, {icon: marker}).addTo(_geolocationMap);
			_geolocationMap.setView(coords);
		}

		// Affiche la carte.
		function show_map()
		{
			App.hide('#diaporama_geolocation_none');
			App.text('#diaporama_geolocation_coords p', data.geolocation_coords);
			App.show('#diaporama_geolocation_map,#diaporama_geolocation_coords');
		}

		// Initialisation.
		if (_q('#leaflet_js'))
		{
			if (is_coords)
			{
				if (_geolocationMap)
				{
					if (reset)
					{
						_geolocationMap.remove();
						_geolocationMap = null;
						set_map();
					}
					else
					{
						show_map();
						set_marker();
					}
				}
				else
				{
					set_map();
				}
			}
			else
			{
				hide_map();
			}
		}
		else if (is_coords)
		{
			load_file(
				'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css',
				'sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=',
				'link',
				'leaflet_css'
			);
			load_file(
				'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js',
				'sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=',
				'script',
				'leaflet_js'
			);
			App.on('#leaflet_js', 'load', set_map);
		}
		else
		{
			hide_map();
		}
	}

	/**
	 * Récupération des données depuis le serveur.
	 *
	 * @param bool start
	 *   Utilise-t-on cete fonction pour la première fois ?
	 * @param bool only_item_infos
	 *   Ne changer que les informations du fichier courant ?
	 * @param int item_position
	 *   Position du fichier courant dans la série.
	 *   0 pour ne pas récupérer d'informations du fichier.
	 * @param int carousel_position
	 *   Position dans le carrousel.
	 *   0 pour ne pas récupérer d'informations du carrousel.
	 *
	 * @return void
	 */
	function _getData(start, only_item_infos, item_position, carousel_position)
	{
		if (start)
		{
			App.show('#diaporama_loading');
		}

		_carouselCurrentPosition = carousel_position > 0
			? carousel_position
			: item_position;

		const infos_div = _q('.diaporama_sidebar[data-name="informations"] > div');

		function error(r)
		{
			App.hide(infos_div);
			console.log(r.message);
			_alertError(r.message);
		}

		function success(r)
		{
			if (!_init)
			{
				_l10n = r.l10n;
				_params = r.params;

				// Texte localisé de l'interface.
				_insertText();

				// Gestion de la lecture automatique.
				_setAutoEvents();

				// Gestion des boutons.
				_setButtonsEvents();

				// Gestion du carrousel.
				_setCarouselEvents();

				// Gestion des favoris et de la sélection.
				_setFavoritesEvents();
				_setSelectionEvents();

				// Gestion des panneaux latéraux.
				_setSidebarsEvents();

				// Gestion de l'affichage des barres de contrôle.
				_setControlBarsEvents();

				// Gestion des votes.
				_setRatingEvents(r);

				// Gestion du swipe.
				_setSwipeEvents();

				// Gestion du zoom.
				_setZoomEvents();

				// Gestion du click sur l'image.
				_setClickEvents();

				// Option pour afficher les informations par défaut.
				if (_options.showInformations && _options.controlBars)
				{
					setTimeout(() =>
					{
						const animate = _options.animate;
						_options.animate = false;
						_icon('informations').click();
						_options.animate = animate;
					}, 100);
				}
			}

			App.show(infos_div);

			// Gestion du carrousel.
			function carousel()
			{
				// Informations des vignettes du carrousel.
				_carouselThumbs = r.carousel;

				// Si le nombre de fichiers de la section courante
				// n'est plus le même, on recharge le diaporama.
				if (_itemsCount > 0 && r.count != _itemsCount)
				{
					_reload(_currentPosition);
				}

				// Si des fichiers ont été supprimés, on recharge le diaporama.
				if (_carouselThumbs[carousel_position] === undefined && r.count > 0)
				{
					_reload(parseInt(r.count));
					return;
				}

				_itemsCount = r.count;
				_carouselChangeThumbs();
			}
			if (carousel_position)
			{
				carousel();
			}

			// Si on ne doit pas récupérer les informations du fichier courant,
			// on ne va pas plus loin.
			if (!item_position)
			{
				return;
			}

			// Change uniquement les informations du fichier courant ?
			if (only_item_infos)
			{
				_items = r.items;
				_changeItemInfos();
				return;
			}

			// Si la série ne contient plus aucun fichier,
			// on vide l'interface du diaporama.
			if (r.count == 0)
			{
				App.empty(
					'#diaporama_item,' +
					'#diaporama_breadcrumb,' +
					'#diaporama_position span,' +
					'#diaporama_carousel_thumbs,' +
					'#diaporama_auto_message,' +
					'#diaporama_auto_mode,' +
					'#diaporama_text,' +
					'.diaporama_sidebar_inner'
				);

				App.remove(
					'.diaporama_icon[data-name="download"],' +
					'.diaporama_icon[data-name="favorite"],' +
					'.diaporama_icon[data-name="selection"]'
				);

				App.addClass(
					'#diaporama_switch,' +
					'a[data-name^="carousel"],' +
					'#diaporama_navigation > a',
					'disabled'
				);

				App.removeClass('#diaporama_switch', 'fullsize');

				_carouselThumbs = undefined;
				_currentPosition = 0;
			}

			// Si la série ou le fichier courant ont été modifié,
			// on recrée tous les fichiers.
			let remake = false;
			for (const pos in r.items)
			{
				if (_items[pos] === undefined || _items[pos].md5 == r.items[pos].md5)
				{
					continue;
				}

				if (_currentPosition > r.count)
				{
					_currentPosition = r.count;
				}

				_deleteItems(true);
				remake = true;
				break;
			}

			_items = r.items;
			_itemsCount = r.count;

			// Création des fichiers.
			const preload = [1, _currentPosition, _itemsCount];
			function preload_push(p)
			{
				if (p > 1 && p < _itemsCount && !preload.includes(p))
				{
					preload.push(p);
				}
			}
			for (let i = 1; i <= _options.itemPreload; i++)
			{
				preload_push(i + 1);
				preload_push(_itemsCount - i);
				preload_push(_currentPosition - i);
				preload_push(_currentPosition + i);
			}
			for (const pos in r.items)
			{
				if (!preload.includes(parseInt(pos)))
				{
					continue;
				}

				const current = _currentPosition == pos;

				if ((!start && !remake) && current)
				{
					continue;
				}

				_createItem(pos);
				if (!_swipeActive)
				{
					_changeItemSizePosition(pos, false, current); 
				}

				if ((start || remake) && current)
				{
					_changeItemInfos();
				}
			}

			// Boutons de navigation.
			_changeButtonsNavigation();

			// On indique de regénérer le carrousel.
			if (remake)
			{
				App.empty('#diaporama_carousel_thumbs');
			}

			// Suppression des fichiers inutiles.
			_deleteItems();

			// Démarrage de la lecture automatique au lancement ?
			if (start && _options.autoStart)
			{
				_icon('auto').click();
			}

			// On met à jour les informations (notamment pour le nombre de vues).
			_changeItemInfos();

			// Changement des vignettes du carrousel.
			_carouselMaxPosition = 0;
			_carouselChangeThumbs();
		}

		App.ajax(
		{
			section: 'diaporama',
			q: _query,
			get_item: item_position > 0 ? 1 : 0,
			get_carousel: carousel_position > 0 ? 1 : 0,
			key: DIAPORAMA.key,
			thumb_size: DIAPORAMA.thumb_size,
			item_position: item_position,
			item_limit: _options.itemLimit,
			carousel_position: _carouselCurrentPosition,
			start: start ? 1 : 0
		},
		{
			dflt: r => console.log(r),
			error: error,
			success: success
		});
	}

	/**
	 * Retourne les dimensions des éléments du diaporama.
	 *
	 * @return object
	 */
	function _getDiaporamaSize()
	{
		const s = {barBottomHeight: 0, barTopHeight: 0, carouselHeight: 0, sidebarWidth: 0};

		// Largeur du panneau latéral.
		App.each('.diaporama_sidebar', elem =>
		{
			if (_options.sidebarItemResize && elem.checkVisibility())
			{
				s.sidebarWidth = elem.offsetWidth;
			}
		});

		// Hauteur du carrousel.
		if (_options.carousel && _options.controlBars)
		{
			s.carouselHeight = _q('#diaporama_carousel').offsetHeight;
		}

		// Hauteur des barres du haut et du bas.
		if (_options.controlBars)
		{
			s.barTopHeight = _q('#diaporama_top').offsetHeight;
			s.barBottomHeight = _q('#diaporama_bottom').offsetHeight;
		}

		// Dimensions de la zone d'affichage.
		s.availableHeight = window.innerHeight - s.barTopHeight
			- s.barBottomHeight - s.carouselHeight;
		s.availableHeight = s.availableHeight < 0 ? 0 : s.availableHeight;
		s.availableWidth = window.innerWidth - s.sidebarWidth;
		s.availableWidth = s.availableWidth < 0 ? 0 : s.availableWidth;

		return s;
	}

	/**
	 * Insère le code HTML du diaporama.
	 *
	 * @return void
	 */
	function _insertHTML()
	{
		App.append('body',
			'<div id="diaporama">' +
				'<div id="diaporama_top" class="diaporama_control_bar">' +
					'<div id="diaporama_home"></div>' +
					'<div id="diaporama_breadcrumb">' +
						'<span id="diaporama_breadcrumb_parents"></span>' +
						'<span id="diaporama_breadcrumb_item"></span>' +
					'</div>' +
					'<div id="diaporama_tools">' +
						'<a href="javascript:;" class="diaporama_icon" data-name="close">' +
							'<span>&#xea0f;</span>' +
						'</a>' +
					'</div>' +
				'</div>' +
				'<div id="diaporama_favorite"></div>' +
				'<div id="diaporama_selection"></div>' +
				'<div id="diaporama_loading"></div>' +
				'<div id="diaporama_text">' +
					'<div id="diaporama_text_inner">' +
						'<p id="diaporama_title"></p>' +
						'<p id="diaporama_description"></p>' +
					'</div>' +
				'</div>' +
				'<div id="diaporama_inner">' +
					'<div id="diaporama_item"></div>' +
					'<div id="diaporama_auto_message"></div>' +
				'</div>' +
				'<section id="diaporama_keyboard">' +
					'<div id="diaporama_keyboard_inner">' +
						'<h1><span>&#xe909;</span></h1>' +
						'<div id="diaporama_keyboard_keys"></div>' +
					'</div>' +
				'</section>' +
				'<section class="diaporama_sidebar" data-name="votes">' +
					'<h1></h1>' +
					'<div class="diaporama_sidebar_inner" tabindex="-1">' +
						'<div id="diaporama_note">' +
							'<span></span>' +
							'<p>' +
								'<span id="diaporama_note_rating"></span>' +
								'<span id="diaporama_note_formated"></span>' +
							'</p>' +
						'</div>' +
						'<div id="diaporama_user_note">' +
							'<span></span>' +
							'<p>' +
								'<span id="diaporama_user_note_rating"><a href="javascript:;"' +
									' data-rating="1"></a><a href="javascript:;"' +
									' data-rating="2"></a><a href="javascript:;"' +
									' data-rating="3"></a><a href="javascript:;"' +
									' data-rating="4"></a><a href="javascript:;"' +
									' data-rating="5"></a></span>' +
								'<a href="javascript:;"' +
									' id="diaporama_user_note_delete">&#xe916;</a>' +
							'</p>' +
						'</div>' +
						'<div id="diaporama_votes_disabled" class="diaporama_info">' +
							'<p></p>' +
						'</div>' +
					'</div>' +
				'</section>' +
				'<section class="diaporama_sidebar" data-name="edit">' +
					'<h1></h1>' +
					'<div class="diaporama_sidebar_inner" tabindex="-1">' +
						'<form>' +
							'<p class="field">' +
								'<label for="diaporama_edit_title"></label>' +
								'<input required id="diaporama_edit_title" class="large"' +
									' type="text" maxlength="255" size="40">' +
							'</p>' +
							'<p class="field">' +
								'<label for="diaporama_edit_filename"></label>' +
								'<input required id="diaporama_edit_filename" class="large"' +
									' type="text" maxlength="255" size="40">' +
							'</p>' +
							'<p class="field">' +
								'<label for="diaporama_edit_desc"></label>' +
								'<textarea id="diaporama_edit_desc" class="large"' +
									' rows="10" cols="50"></textarea>' +
							'</p>' +
							'<p class="field">' +
								'<label for="diaporama_edit_tags"></label>' +
								'<textarea id="diaporama_edit_tags" class="large"' +
									' rows="6" cols="50"></textarea>' +
							'</p>' +
							'<div class="diaporama_buttons">' +
								'<input type="submit">' +
							'</div>' +
						'</form>' +
					'</div>' +
				'</section>' +
				'<section class="diaporama_sidebar" data-name="informations">' +
					'<h1></h1>' +
					'<div class="diaporama_sidebar_inner" tabindex="-1">' +
						'<ul></ul>' +
					'</div>' +
				'</section>' +
				'<section class="diaporama_sidebar" data-name="geolocation">' +
					'<h1></h1>' +
					'<div class="diaporama_sidebar_inner" tabindex="-1">' +
						'<div id="diaporama_geolocation_map"></div>' +
						'<div id="diaporama_geolocation_coords">' +
							'<p></p>' +
						'</div>' +
						'<div id="diaporama_geolocation_none" class="diaporama_info">' +
							'<p></p>' +
						'</div>' +
					'</div>' +
				'</section>' +
				'<section class="diaporama_sidebar" data-name="settings">' +
					'<h1></h1>' +
					'<div class="diaporama_sidebar_inner" tabindex="-1">' +
						'<div data-mobile="0">' +
							'<h2></h2>' +
							'<p class="field">' +
								'<label for="diaporama_transitions_effect"></label>' +
								'<select id="diaporama_transitions_effect"></select>' +
							'</p>' +
							'<p class="field">' +
								'<label for="diaporama_transitions_duration"></label>' +
								'<input id="diaporama_transitions_duration" type="text"' +
									' maxlength="4" size="4">' +
							'</p>' +
						'</div>' +
						'<div>' +
							'<h2></h2>' +
							'<p class="field" data-mobile="1">' +
								'<label for="diaporama_auto_duration_mobile"></label>' +
								'<span id="diaporama_auto_duration_option">' +
									'<input id="diaporama_auto_duration_mobile" type="range"' +
										' min="1" max="10" step="0.5" value="3">' +
									'<span>3.0 s</span>' +
								'</span>' +
							'</p>' +
							'<p class="field">' +
								'<input id="diaporama_auto_start" type="checkbox">' +
								' <label for="diaporama_auto_start"></label>' +
							'</p>' +
							'<p class="field">' +
								'<input id="diaporama_auto_loop" type="checkbox">' +
								' <label for="diaporama_auto_loop"></label>' +
							'</p>' +
						'</div>' +
						'<div>' +
							'<h2></h2>' +
							'<p class="field" data-mobile="0">' +
								'<input id="diaporama_control_bars" type="checkbox">' +
								' <label for="diaporama_control_bars"></label>' +
							'</p>' +
							'<p class="field" data-mobile="0">' +
								'<input id="diaporama_carousel_option" type="checkbox">' +
								' <label for="diaporama_carousel_option"></label>' +
							'</p>' +
							'<p class="field">' +
								'<input id="diaporama_over_image_title" type="checkbox">' +
								' <label for="diaporama_over_image_title"></label>' +
							'</p>' +
							'<p class="field">' +
								'<input id="diaporama_over_image_description" type="checkbox">' +
								' <label for="diaporama_over_image_description"></label>' +
							'</p>' +
						'</div>' +
						'<div>' +
							'<h2></h2>' +
							'<p class="field">' +
								'<input id="diaporama_full_screen" type="checkbox">' +
								' <label for="diaporama_full_screen"></label>' +
							'</p>' +
							'<p class="field" data-mobile="0">' +
								'<input id="diaporama_show_informations" type="checkbox">' +
								' <label for="diaporama_show_informations"></label>' +
							'</p>' +
						'</div>' +
						'<div data-mobile="0" id="diaporama_keyboard_k">' +
							'<h2></h2>' +
							'<p></p>' +
						'</div>' +
					'</div>' +
				'</section>' +
				'<div id="diaporama_carousel">' +
					'<a class="disabled" data-name="carousel_prev" tabindex="-1">' +
						'<span>&#xf104;</span>' +
					'</a>' +
					'<div id="diaporama_carousel_thumbs"></div>' +
					'<a class="disabled" data-name="carousel_next" tabindex="-1">' +
						'<span>&#xf105;</span>' +
					'</a>' +
				'</div>' +
				'<div id="diaporama_bottom" class="diaporama_control_bar">' +
					'<div id="diaporama_position"><span></span></div>' +
					'<div id="diaporama_navigation">' +
						'<a class="disabled" data-name="first"><span>&#xf104;&#xf104;</span></a>' +
						'<a class="disabled" data-name="prev"><span>&#xf104;</span></a>' +
						'<a class="disabled" id="diaporama_switch"><span>.</span></a>' +
						'<a class="disabled" data-name="next"><span>&#xf105;</span></a>' +
						'<a class="disabled" data-name="last"><span>&#xf105;&#xf105;</span></a>' +
					'</div>' +
					'<div id="diaporama_auto_mode">' +
						'<a data-name="auto" data-status="start"><span>&#xf04b;</span></a>' +
						'<span id="diaporama_auto_duration">0.0 s</span>' +
						'<a data-name="minus"><span>&#xf068;</span></a>' +
						'<a data-name="plus"><span>&#xf067;</span></a>' +
					'</div>' +
				'</div>' +
			'</div>'
		);
		App.attr('#diaporama_bottom a', 'href', 'javascript:;');
		App.addClass('#diaporama_bottom a', 'diaporama_icon');
		_q('#diaporama_carousel').style.display = _options.carousel ? 'block' : 'none';

		// Fermeture du diaporama.
		App.click(_icon('close'), () =>
		{
			App.off(window, 'resize', _resize);

			// On réinitialise le zoom.
			_zoom = {};

			// On désactive les événements clavier.
			_keyboardActive = false;

			// On remet en place les barres de défilement.
			_q('html').style.overflow = _overflow.html;
			_q('body').style.overflow = _overflow.body;

			// On quitte le mode plein écran.
			_fullScreenExit();

			// On stoppe la lecture automatique.
			if (_autoActive)
			{
				_icon('auto').click();
				_autoChangeMessage('STOP');
			}

			// On stoppe la lecture vidéo.
			App.each('#diaporama video', elem => elem.pause());

			// On cache le diaporama et on réaffiche la galerie.
			App.show('#gallery');
			App.hide('#diaporama');
			window.scroll(0, _overflow.scroll);

			// Focus sur le lien du diaporama.
			_link.focus();
		});
	}

	/**
	 * Insère le texte localisé et le reste du code HTML.
	 *
	 * @return void
	 */
	function _insertText()
	{
		// Lien vers la page d'accueil de la galerie.
		App.prepend('#diaporama_home',
			'<a class="diaporama_icon" data-name="home">' +
				'<span>&#xe906;</span>' +
			'</a>'
		);
		App.attr('#diaporama_home a', 'href', GALLERY.path + '/');

		// Icônes d'outils.
		const tools =
		{
			settings: '&#xf013;',
			geolocation: '&#xe94b;',
			informations: '&#xf05a;',
			edit: '&#xe962;',
			votes: '&#xe9d9;',
			favorite: '&#xe910;',
			selection: '&#xea52;',
			download: '&#xe9c7;'
		};
		for (const name in tools)
		{
			if ((name == 'favorite' && !_params.favorites)
			 || (name == 'selection' && !_params.selection))
			{
				continue;
			}
			const icon =
				`<a href="javascript:;" class="diaporama_icon" data-name="${name}">` +
					`<span>${tools[name]}</span>` +
				`</a>`;
			App.prepend('#diaporama_tools', icon);
		}
		App.attr('.diaporama_icon[data-name="close"]', 'title', _l10n.close);
		_icon('geolocation').style.display = _params.geolocation ? 'inline' : 'none';
		_icon('votes').style.display = _params.votes ? 'inline' : 'none';

		// Panneaux latéraux.
		for (const name of ['edit', 'geolocation', 'informations', 'settings', 'votes'])
		{
			// Icône.
			const elem = _q(`.diaporama_icon[data-name="${name}"]`);
			App.attr(elem, 'title', _l10n[name]);
			App.addClass(elem, 'diaporama_sidebar_icon');

			// Titre du panneau.
			const h1 = _q(`.diaporama_sidebar[data-name="${name}"] h1`)
			App.text(h1, _l10n[name]);
			App.prepend(h1,
				'<span class="diaporama_sidebar_close"></span>' +
				'<span class="diaporama_sidebar_close_icon"></span>'
			);
		}
		if (!_params.tags)
		{
			App.remove(_q('#diaporama_edit_tags').closest('p'));
		}

		// Ajout d'une note.
		App.text('#diaporama_note > span', _l10n.stats_rating);
		App.text('#diaporama_user_note > span', _l10n.rating_user);
		App.attr('#diaporama_user_note_delete', 'title', _l10n.delete);
		App.text('#diaporama_votes_disabled p', _l10n.rating_disabled);

		// Édition.
		for (const name of ['title', 'filename', 'desc', 'tags'])
		{
			App.text(`label[for="diaporama_edit_${name}"]`, _l10n['edit_' + name]);
		}
		App.val('.diaporama_buttons input[type="submit"]', _l10n.save);

		// Géolocalisation.
		App.text('#diaporama_geolocation_none p', _l10n.geolocation_none);

		// Options.
		const settings = _q('.diaporama_sidebar[data-name="settings"]');
		const settings_blocs =
		[
			_l10n.transition,
			_l10n.auto,
			_l10n.display,
			_l10n.at_launch,
			_l10n.keyboard_control
		];
		for (let i = 0; i < settings_blocs.length; i++)
		{
			App.text(
				[settings, `.diaporama_sidebar_inner > div:nth-child(${i + 1}) h2`],
				settings_blocs[i]
			);
		}

		// Balises <label>.
		const labels =
		[
			'auto_duration_mobile',
			'auto_loop',
			'auto_start',
			'carousel_option',
			'control_bars',
			'full_screen',
			'over_image_description',
			'over_image_title',
			'show_informations',
			'transitions_duration',
			'transitions_effect'
		];
		for (const name of labels)
		{
			App.text(`label[for="diaporama_${name}"]`, _l10n[name]);
		}

		// Transition entre les photos.
		const effect = _q('#diaporama_transitions_effect');
		for (const name in _l10n.effects)
		{
			const option = document.createElement('option');
			option.value = name;
			App.text(option, _l10n.effects[name]);
			effect.append(option);
		}
		const selected = _q(effect, `option[value="${_options.transitionEffect}"]`);
		effect.selectedIndex = selected ? selected.index : 1;
		App.on(effect, 'change', evt =>
		{
			_options.transitionEffect = evt.target.value;
			_savePrefs();
		});

		// Navigation au clavier.
		const tip = _l10n.keyboard_tip.split('%s');
		const p = _q('#diaporama_keyboard_k p');
		App.appendText(p, tip[0]);
		App.append(p, '<kbd>K</kbd>');
		App.appendText(p, tip[1]);
		App.appendText('#diaporama_keyboard h1', _l10n.keyboard_control);

		// Touches pour la navigation au clavier.
		const keys =
		[
			{
				h2: 'navigation',
				ul:
				[
					{kbd: '&#x2190;', text: 'previous_photo'},
					{kbd: '&#x2192;', text: 'next_photo'},
					{kbd: 'first_photo_key', text: 'first_photo'},
					{kbd: 'last_photo_key', text: 'last_photo'}
				]
			},
			{
				h2: 'auto',
				ul:
				[
					{kbd: 'space_key', text: 'space'},
					{kbd: '-', text: 'decrease_display_time'},
					{kbd: '+', text: 'increase_display_time'}
				]
			},
			{
				h2: 'informations',
				ul:
				[
					{kbd: 'T', text: 'title'},
					{kbd: 'D', text: 'description'},
					{kbd: 'I', text: 'informations'},
					{kbd: 'G', text: 'geolocation'}
				]
			},
			{
				h2: 'carousel_option',
				ul:
				[
					{kbd: 'C', text: 'carousel'},
					{kbd: 'carousel_prev_key', text: 'carousel_prev'},
					{kbd: 'carousel_next_key', text: 'carousel_next'}
				]
			},
			{
				h2: 'other_functions',
				ul:
				[
					{kbd: 'B', text: 'control'},
					{kbd: 'Z', text: 'size'},
					{kbd: 'F', text: 'favorites'},
					{kbd: 'S', text: 'selection'}
				]
			},
			{
				h2: 'special',
				ul:
				[
					{kbd: 'K', text: 'keyboard'},
					{kbd: 'close_key', text: 'close'}
				]
			}
		];
		for (const list of keys)
		{
			const div = document.createElement('div');
			const h2 = document.createElement('h2');
			App.text(h2, _l10n[list.h2]);
			div.append(h2);
			const ul = document.createElement('ul');
			for (const key of list.ul)
			{
				if ((key.kbd == 'F' && !_params.favorites)
				 || (key.kbd == 'S' && !_params.selection)
				 || (key.kbd == 'G' && !_params.geolocation))
				{
					continue;
				}
				const li = document.createElement('li');
				const kbd = document.createElement('kbd');
				const kbd_code = _l10n.keyboard_keys[key.kbd] || key.kbd;
				if (kbd_code.match(/^&#x[0-9A-F]{1,6};$/))
				{
					App.prepend(kbd, kbd_code);
				}
				else
				{
					App.text(kbd, kbd_code);
				}
				li.append(kbd);
				li.append(' ' + _l10n.keyboard_keys[key.text]);
				ul.append(li);
			}
			div.append(ul);
			_q('#diaporama_keyboard_keys').append(div);
		}

		// On indique que le diaporama a été initialisé.
		_init = true;
	}

	/**
	 * Effectue la transition entre le fichier courant et le fichier demandé.
	 *
	 * @param int old_position
	 * @param int new_position
	 * @param string button
	 *
	 * @return void
	 */
	function _itemTransition(old_position, new_position, button)
	{
		const item_old = _q('#diaporama_item_' + old_position);
		const item_new = _q('#diaporama_item_' + new_position);
		const s = _getDiaporamaSize();

		_animationActive = true;

		let transition = _options.transitionEffect;
		if (_options.transitionEffect == 'random')
		{
			const effects = _q('#diaporama_transitions_effect');
			const count = Object.keys(_qAll(effects, 'option')).length - 2;
			const rand = Math.floor(Math.random() * count) + 1;
			transition = effects.getElementsByTagName('option')[rand].value;
		}

		let duration = parseInt(_options.transitionDuration),
			options = {duration: duration, easing: 'ease-in-out'},
			old_css = {display: 'none'}, old_css_anime = {},
			new_css = {display: 'block'}, new_css_anime = {};

		function anime(elem, css, callback)
		{
			App.animate(elem, css, options, callback);
		}

		function finish()
		{
			_animationActive = false;
			_getData(false, false, _currentPosition, _currentPosition);
		}

		function get_prop(elem, prop)
		{
			return parseFloat(getComputedStyle(elem)[prop]);
		}

		function new_style(now = true)
		{
			setTimeout(_changeItemInfos, now ? 0 : (duration / 2));
			Object.assign(item_new.style, new_css);
			anime(item_new, new_css_anime, () =>
			{
				Object.assign(item_new.style, new_css_anime);
				finish();
			});
		}

		switch (transition)
		{
			case 'curtainX' :
			case 'curtainY' :
			case 'zoom' :
				options.duration /= 2;
				function curtain(elem, css1, css2)
				{
					function css(prop1, prop2, a)
					{
						css1[prop1] = 0;
						css1[prop2] = (a / 2) + 'px';
						css2[prop1] = get_prop(elem, prop1) + 'px';
						css2[prop2] = get_prop(elem, prop2) + 'px';
					}
					if (transition != 'curtainX')
					{
						css('height', 'top', s.availableHeight);
					}
					if (transition != 'curtainY')
					{
						css('width', 'left', s.availableWidth);
					}
				}
				curtain(item_old, old_css_anime, old_css);
				anime(item_old, old_css_anime, () =>
				{
					Object.assign(item_old.style, old_css);
					curtain(item_new, new_css, new_css_anime);
					new_style();
				});
				break;

			case 'fade' :
				anime(item_old, {opacity: 0}, () => Object.assign(item_old.style, old_css));
				new_css.opacity = 0;
				new_css_anime.opacity = 1;
				new_style(false);
				break;

			case 'puff' :
				options.duration /= 2;
				function puff(elem, css1, css2)
				{
					css2.height = (get_prop(elem, 'height') * 1.5) + 'px';
					css2.width = (get_prop(elem, 'width') * 1.5) + 'px';
					css2.left = ((s.availableWidth - (get_prop(elem, 'width') * 1.5)) / 2) + 'px';
					css2.top = (((s.availableHeight - (get_prop(elem, 'height') * 1.5)) / 2)
						+ s.barTopHeight) + 'px';
					css2.opacity = 0;
					css1.opacity = 1;
				}
				puff(item_old, old_css, old_css_anime);
				anime(item_old, old_css_anime, () =>
				{
					Object.assign(item_old.style, old_css);
					new_css_anime = _changeItemSizePosition(new_position, true, true, true);
					puff(item_new, new_css_anime, new_css);
					new_style();
				});
				break;

			case 'slideX' :
			case 'slideY' :
				const nl = button == 'next' || button == 'last';
				const w = s.availableWidth;
				function slide(elem, css1, css2, a)
				{
					const prop = transition == 'slideX' ? 'left' : 'top';
					css1[prop] = (get_prop(elem, prop) + a) + 'px';
					css2[prop] = get_prop(elem, prop) + 'px';
				}
				slide(item_old, old_css_anime, old_css, nl ? -w : w);
				anime(item_old, old_css_anime, () => Object.assign(item_old.style, old_css));
				slide(item_new, new_css, new_css_anime, nl ? w : -w);
				new_style(false);
				break;

			case 'slideXLeft' :
			case 'slideYBottom' :
				options.duration /= 2;
				options.easing = 'ease-out';
				function diapo(elem, css1, css2)
				{
					function css(prop, a)
					{
						const val = get_prop(elem, prop);
						css1[prop] = val + 'px';
						css2[prop] = (prop == 'top' ? (val + a) : -(val + a)) + 'px';
					}
					if (transition == 'slideXLeft')
					{
						css('left', s.availableWidth);
					}
					else
					{
						css('top', s.availableHeight);
					}
				}
				diapo(item_old, old_css, old_css_anime);
				anime(item_old, old_css_anime, () =>
				{
					Object.assign(item_old.style, old_css);
					diapo(item_new, new_css_anime, new_css);
					new_style();
				});
				break;

			default :
				App.hide(item_old);
				App.show(item_new);
				_changeItemInfos();
				finish();
				break;
		}
	}

	/**
	 * Gestion du contrôle au clavier.
	 *
	 * @return void
	 */
	function _keyboard()
	{
		if (!_options.keyboard)
		{
			App.hide('#diaporama_keyboard_k');
			return;
		}

		let ctrl = false, ctrl_timer;

		// On désactive le contrôle au clavier lorsque
		// le focus est sur un élément de formulaire.
		const form_elements = '#diaporama :is(input,select,textarea)';
		App.on(form_elements,
		{
			'focus': () => _keyboardActive = false,
			'blur': () => _keyboardActive = true
		});
		App.click('#diaporama_inner', () =>
		{
			App.each(form_elements, elem => elem.blur());
		});

		App.on(document, 'keyup', evt =>
		{
			if (!_keyboardActive || _animationActive)
			{
				return;
			}
			if (evt.keyCode == 16 || evt.keyCode == 17 || evt.keyCode == 18 || ctrl)
			{
				clearTimeout(ctrl_timer);
				ctrl_timer = setTimeout(() => ctrl = false, 1000);
				return;
			}

			switch (evt.keyCode)
			{
				// Echap : quitte le diaporama.
				case 27 :
					_icon('close').click();
					break;

				// Espace : démarre ou arrête la lecture automatique.
				case 32 :
					_icon('auto').click();
					break;

				// Haut de page : page de vignette précédente.
				case 33 :
					_icon('carousel_prev').click();
					break;

				// Bas de page : page de vignette suivante.
				case 34 :
					_icon('carousel_next').click();
					break;

				// Fin : dernier fichier.
				case 35 :
					_icon('last').click();
					break;

				// Début : premier fichier.
				case 36 :
					_icon('first').click();
					break;

				// Flèche gauche : fichier précédent.
				case 37 :
					_icon('prev').click();
					break;

				// Flèche droite : fichier suivant.
				case 39 :
					_icon('next').click();
					break;

				// B : Barres de contrôle.
				case 66 :
					if (!_options.controlBars)
					{
						break;
					}
					if (_q('.diaporama_control_bar').checkVisibility())
					{
						App.hide('.diaporama_control_bar');
						if (_options.carousel)
						{
							App.hide('#diaporama_carousel');
						}
						App.addClass('#diaporama', 'nocontrols');
					}
					else
					{
						App.show('.diaporama_control_bar', 'flex');
						if (_options.carousel)
						{
							App.show('#diaporama_carousel');
						}
						App.removeClass('#diaporama', 'nocontrols');
					}
					_resize();
					break;

				// C : Carrousel.
				case 67 :
					_q('#diaporama_carousel_option').click();
					break;

				// D : Description sur les images.
				case 68 :
					_q('#diaporama_over_image_description').click();
					break;

				// F : Ajoute le fichier aux favoris.
				case 70 :
					_icon('favorite').click();
					break;

				// G et I : Panneaux de géolocalisation et d'informations.
				case 71 :
				case 73 :
					const icon = evt.keyCode == 71 ? 'geolocation' : 'informations';
					if (_icon(icon).style.display != 'none' && !_animationActive
					&& (_q('#diaporama_top').checkVisibility()
					|| App.hasClass('#diaporama', 'nocontrols')))
					{
						_icon(icon).click();
					}
					break;

				// K : Affiche la liste des touches pour le contrôle au clavier.
				case 75 :
					const keyboard = _q('#diaporama_keyboard');
					keyboard.style.display = keyboard.checkVisibility() ? 'none' : 'flex';
					break;

				// S : Ajoute le fichier à la sélection.
				case 83 :
					_icon('selection').click();
					break;

				// T : Titre sur les images.
				case 84 :
					_q('#diaporama_over_image_title').click();
					break;

				// Sauvegarde la nouvelle durée d'affichage.
				case 107 :
				case 109 :
					_savePrefs();
					break;

				// Z ou point (pavé numérique) : switch taille réelle / redimensionnée.
				case 90 :
				case 110 :
					_q('#diaporama_switch').click();
					break;
			}
		});

		App.on(document, 'keydown', evt =>
		{
			if (!_keyboardActive || ctrl)
			{
				return;
			}

			switch (evt.keyCode)
			{
				// Si les touches Alt, Ctrl ou Maj sont enfoncées,
				// on désactive le contrôle au clavier.
				case 16 :
				case 17 :
				case 18 :
					clearTimeout(ctrl_timer);
					ctrl = true;
					break;

				// Signe plus du pavé numérique :
				// augmente la durée d'affichage de la lecture automatique.
				case 107 :
					_autoDurationMouseDown = true;
					_autoChangeDuration(_options.autoDurationPrecision);
					_autoDurationMouseDown = false;
					break;

				// Signe moins du pavé numérique :
				// diminue la durée d'affichage de la lecture automatique.
				case 109 :
					_autoDurationMouseDown = true;
					_autoChangeDuration(-_options.autoDurationPrecision);
					_autoDurationMouseDown = false;
					break;
			}
		});
	}

	/**
	 * Recharge tout le diaporama depuis la position p.
	 *
	 * @param int p
	 *
	 * @return void
	 */
	function _reload(p)
	{
		_currentPosition = p;
		_itemsCount = 0;
		_realsize = false;
		_changeButtonsNavigation();
		_deleteItems(true);
		_getData(true, false, p, p);
	}

	/**
	 * Redimensionne le diaporama et le fichier courant
	 * lors du redimensionnement du navigateur.
	 *
	 * @return void
	 */
	function _resize()
	{
		const animate = _options.animate;
		_options.animate = false;

		if (_items[_currentPosition] !== undefined)
		{
			_changeItemSizePosition(_currentPosition);
			_sidebarsIcons();
		}

		_carouselChangeSizePosition();
		_changeCenterSizePosition();
		_previousDiaporamaSize = _getDiaporamaSize();
		_options.animate = animate;
	}

	/**
	 * Sauvegarde les préférences utilisateur.
	 *
	 * @return void
	 */
	function _savePrefs()
	{
		const prefs =
		{
			autoDuration: _options.autoDuration,
			autoLoop: _options.autoLoop,
			autoStart: _options.autoStart,
			carousel: _options.carousel,
			controlBars: _options.controlBars,
			fullScreen: _options.fullScreen,
			overImageDescription: _options.overImageDescription,
			overImageTitle: _options.overImageTitle,
			sidebarInformations: JSON.stringify(_options.sidebarInformations),
			showInformations: _options.showInformations,
			transitionDuration: _options.transitionDuration,
			transitionEffect: JSON.stringify(_options.transitionEffect)
		};
		document.cookie = 'igal3_diaporama=' + JSON.stringify(prefs)
			+ '; expires=Fri, 31 Dec 9999 23:59:59 GMT; path=' + GALLERY.path;
	}

	/**
	 * Gestionnaires d'événements pour la lecture automatique.
	 *
	 * @return void
	 */
	function _setAutoEvents()
	{
		// Gestion du bouton de lecture automatique.
		App.click(_icon('auto'), () =>
		{
			// La lecture automatique est-elle en cours ?
			if (_autoActive)
			{
				_autoActive = false;

				// On stoppe la lecture automatique.
				clearTimeout(_autoTimer);
				_autoTimer = undefined;

				// Message indiquant l'arrêt de la lecture automatique.
				_autoChangeMessage('STOP');

				// Changement de l'icône.
				App.attr(_icon('auto'), 'data-status', 'start');

				return;
			}

			// S'il n'y a pas plus d'un fichier, inutile de démarrer la lecture automatique.
			if (_itemsCount < 2)
			{
				return;
			}

			// Si l'on est au dernier fichier,
			// et que l'option pour boucler est désactivée,
			// on ne démarre pas la lecture automatique.
			if (_currentPosition == _itemsCount && !_options.autoLoop)
			{
				return;
			}

			// Changement de l'icône.
			App.attr(_icon('auto'), 'data-status', 'stop');

			// Message indiquant le démarrage de la lecture automatique.
			_autoChangeMessage('PLAY');

			// On démarre la lecture automatique.
			_autoActive = true;
			_autoChangeItem(true);
		});

		// Augmenter la durée d'affichage.
		App.on(_icon('plus'),
		{
			'click': evt => evt.preventDefault(),
			'mousedown': evt =>
			{
				if (evt.button === 0)
				{
					_autoDurationMouseDown = true;
					_autoChangeDuration(_options.autoDurationPrecision);
				}
			},
			'mouseup': evt =>
			{
				if (evt.button === 0)
				{
					_autoDurationMouseDown = false;
					_savePrefs();
				}
			}
		});

		// Diminuer la durée d'affichage.
		App.on(_icon('minus'),
		{
			'click': evt => evt.preventDefault(),
			'mousedown': evt =>
			{
				if (evt.button === 0)
				{
					_autoDurationMouseDown = true;
					_autoChangeDuration(-_options.autoDurationPrecision);
				}
			},
			'mouseup': evt =>
			{
				if (evt.button === 0)
				{
					_autoDurationMouseDown = false;
					_savePrefs();
				}
			}
		});

		// Réglage de la durée d'affichage sur mobiles.
		App.on('#diaporama_auto_duration_option input', 'input', function()
		{
			_options.autoDuration = parseFloat(this.value);
			_autoTextDuration();
			_savePrefs();
		});
	}

	/**
	 * Gestionnaires d'événements sur les boutons.
	 *
	 * @return void
	 */
	function _setButtonsEvents()
	{
		// Boutons de navigation.
		App.click('#diaporama_navigation a[data-name]', function()
		{
			if (!App.hasClass(this, 'disabled'))
			{
				_changeItem(App.attr(this, 'data-name'), true);
			}
		});

		// Bouton de switch taille réelle / taille redimensionnée.
		App.click('#diaporama_switch', function()
		{
			if (!App.hasClass(this, 'disabled'))
			{
				_switchActive = true;
				_realsize = !_realsize;
				if (_zoom.width)
				{
					_realsize = false;
					_zoom = {};
				}
				_changeItemSizePosition(_currentPosition, true, false, false, 0, () =>
				{
					_switchActive = false;
					_changeButtonSwitch();
					_zoomText();
				});
			}
		});
	}

	/**
	 * Événements pour le carrousel.
	 *
	 * @return void
	 */
	function _setCarouselEvents()
	{
		function change(d, p, n)
		{
			const a = _q(`a[data-name="carousel_${d}"]`);
			const dl = _q(`#diaporama_carousel_thumbs dl:${p}`);

			if (App.hasClass(a, 'disabled') || _carouselAnimation || !dl.id
			|| !_q('#diaporama_carousel').checkVisibility())
			{
				return;
			}

			const id = parseInt(dl.id.replace(/diaporama_carousel_image_/, ''));
			_carouselMaxPosition = 0;
			_carouselCurrentPosition = id - n;
			_carouselChangeThumbs(d);
		}

		App.click('a[data-name="carousel_prev"]', () =>
		{
			change('prev', 'first-child', 1)
		});
		App.click('a[data-name="carousel_next"]', () =>
		{
			change('next', `nth-child(${(Math.floor(_carouselMaxThumbs))})`, 0);
		});
	}

	/**
	 * Gestion du click sur une image.
	 *
	 * @return void
	 */
	function _setClickEvents()
	{
		if (!_options.clickZoom)
		{
			return;
		}

		function clickmove()
		{
			if (_clickStart)
			{
				_clickMove = true;
			}
		}

		function clickstart(evt)
		{
			if (evt.button === 0 || evt.button === undefined)
			{
				_clickMove = false;
				_clickStart = true;
			}
		}

		App.on('#diaporama_inner',
		{
			'mousedown': clickstart,
			'mousemove': clickmove,
			'mouseup': _clickZoom,
			'touchend': _clickZoom,
			'touchmove': clickmove,
			'touchstart': clickstart
		});
	}

	/**
	 * Gestion de l'affichage des barres de contrôle.
	 *
	 * @return void
	 */
	function _setControlBarsEvents()
	{
		let control_bars_over = false, hide_timer, x, y;

		// Détermine si un panneau latéral est affiché.
		function is_sidebar_visible()
		{
			return [..._qAll('.diaporama_sidebar')].find(elem => elem.checkVisibility());
		}

		// Option par défaut.
		if (!_options.controlBars)
		{
			App.hide('.diaporama_control_bar,#diaporama_carousel');
			_changeCenterSizePosition();
		}

		// Gestion du mouvement de la souris.
		App.on('#diaporama', 'mousemove', evt =>
		{
			if (_options.controlBars || is_sidebar_visible() || control_bars_over
			|| _animationActive || (x == evt.pageX && y == evt.pageY))
			{
				return;
			}

			clearTimeout(hide_timer);

			x = evt.pageX;
			y = evt.pageY;

			// Affiche les barres.
			const options = {duration: 350, fill: 'forwards'};
			let selectors = '.diaporama_control_bar';
			if (_options.carousel)
			{
				selectors += ',#diaporama_carousel';
			}
			_animationActive = true;
			App.each(selectors, elem =>
			{
				App.show(elem, 0, elem.id == 'diaporama_carousel' ? 'block' : 'flex');
				elem.style.opacity = 0;
				App.animate(elem, {opacity: 1}, options, () => _animationActive = false);
			});
			if (_options.carousel)
			{
				_carouselChangeSizePosition();
				_changeCenterSizePosition();
			}

			// Cache les barres.
			hide_timer = setTimeout(() =>
			{
				if (!_options.controlBars && !is_sidebar_visible() && !control_bars_over)
				{
					_animationActive = true;
					App.each(selectors, elem =>
					{
						App.animate(elem, {opacity: 0}, options, () =>
						{
							App.hide(elem);
							_animationActive = false;
						});
					});
				}
			}, 1000);
		});

		// On ne cache pas les barres de contrôle si la souris se trouve sur celles-ci.
		App.on('.diaporama_control_bar,#diaporama_carousel',
		{
			'mouseover': () => control_bars_over = true,
			'mouseout': () => control_bars_over = false
		});
	}

	/**
	 * Gestion des favoris.
	 *
	 * @return void
	 */
	function _setFavoritesEvents()
	{
		if (!_icon('favorite'))
		{
			return;
		}
		App.click(_icon('favorite'), function()
		{
			if (_items[_currentPosition] === undefined)
			{
				return;
			}

			const action = App.hasClass(this, 'active') ? 'remove' : 'add';

			if ((action == 'add' && _items[_currentPosition].user.in_favorites)
			|| ((action == 'remove' && !_items[_currentPosition].user.in_favorites)))
			{
				return;
			}

			App.toggleClass(this, 'active', action == 'add');
			App.toggleClass('#diaporama_favorite', 'in', action == 'add');

			App.ajax(
			{
				section: 'favorites-' + action,
				item_id: _items[_currentPosition].id
			},
			{
				dflt: r => console.log(r),
				error: r => _alertError(r.message),
				success: r =>
				{
					_items[_currentPosition].user.in_favorites = action == 'add' ? 1 : 0;

					App.text('#diaporama_sidebar_stats_favorites span', r.favorites_short);

					if (_q('#item') && DIAPORAMA.item_id == ITEM.id)
					{
						Gallery.updateFavorites?.(action, r);
					}

					if (_query.match(/\/user-favorites\//))
					{
						_getData(false, false, _currentPosition, _currentPosition);
					}
				}
			});
		});
	}

	/**
	 * Gestion des options.
	 *
	 * @param object options
	 *
	 * @return void
	 */
	function _setOptions(options)
	{
		// Options.
		if (typeof options == 'object')
		{
			for (const name in options)
			{
				_options[name] = options[name];
			}
		}

		// Sur mobile, on change certaines options (partie 1).
		const is_mobile = !matchMedia('(pointer:fine)').matches
			&& window.innerWidth <= _options.mobileMaxWidth;
		if (is_mobile)
		{
			_options.fullScreen = _options.fullScreenMobile;
		}

		// Préférences de l'utilisateur.
		try
		{
			let cookie_value = document.cookie
				.replace(/(?:(?:^|.*;\s*)igal3_diaporama\s*\=\s*([^;]*).*$)|^.*$/, '$1');
			if (cookie_value)
			{
				cookie_value = JSON.parse(cookie_value);
				if (typeof cookie_value == 'object')
				{
					for (const name in cookie_value)
					{
						_options[name] = JSON.parse(cookie_value[name]);
					}
				}
			}
		}
		catch (error)
		{
			console.log(error);
		}

		// Sur mobile, on change certaines options (partie 2).
		if (is_mobile)
		{
			_options.autoDurationMax = 10;
			_options.autoDurationPrecision = 0.5;
			_options.carousel = false;
			_options.controlBars = true;
			_options.keyboard = false;
			_options.showInformations = false;
			_options.sidebarItemResize = false;
			_options.transitionDuration = 300;
			_options.transitionEffect = 'fade';
			if (_options.autoDuration > _options.autoDurationMax)
			{
				_options.autoDuration = _options.autoDurationMax;
			}
		}

		// Option "Durée de transition".
		const duration = _q('#diaporama_transitions_duration');
		App.on(duration, 'keyup', () =>
		{
			const val = parseInt(duration.value);
			_options.transitionDuration = isNaN(val) ? 0 : val;
			_savePrefs();
		});
		duration.value = _options.transitionDuration;

		// Option "Afficher le carrousel".
		const carousel = _q('#diaporama_carousel');
		const carousel_option = _q('#diaporama_carousel_option');
		App.on(carousel_option, 'change', () =>
		{
			_options.carousel = !_options.carousel;
			if (_options.carousel)
			{
				_getData(false, false, 0, _carouselCurrentPosition);
				if (_q('#diaporama_top').checkVisibility())
				{
					App.show(carousel);
				}
				_carouselChangeSizePosition();
				_changeCenterSizePosition();
			}
			else
			{
				App.hide(carousel);
				_changeCenterSizePosition();
			}
			_changeItemSizePosition(_currentPosition);
			_previousDiaporamaSize = _getDiaporamaSize();
			_savePrefs();
		});
		if (_options.carousel)
		{
			App.show(carousel);
			carousel_option.checked = true;
			_changeCenterSizePosition();
		}
		else
		{
			App.hide(carousel);
		}
		_options.carouselThumbsSize = _options.carouselThumbsSize < 50
			? 50
			: _options.carouselThumbsSize;

		// Options sous forme de cases à cocher.
		function checkbox(option, callback)
		{
			const input = _q('#diaporama_' + option.replace(/([A-Z])/g, '_$1').toLowerCase());
			App.on(input, 'change', () =>
			{
				_options[option] = !_options[option];
				if (callback)
				{
					callback();
				}
				_savePrefs();
			});
			if (_options[option])
			{
				input.checked = true;
			}
		}
		checkbox('autoStart');
		checkbox('autoLoop', () =>
		{
			if (_currentPosition == _itemsCount && !_options.autoLoop)
			{
				_icon('auto').click();
			}
		});
		checkbox('controlBars', () =>
		{
			_changeItemSizePosition(_currentPosition);
			_changeCenterSizePosition();
		});
		checkbox('overImageTitle', _changeOverImageTextDisplay);
		checkbox('overImageDescription', _changeOverImageTextDisplay);
		checkbox('fullScreen', () =>
		{
			if (_options.fullScreen)
			{
				_fullScreen();
			}
			else
			{
				_fullScreenExit();
			}
		});
		checkbox('showInformations');
	}

	/**
	 * Gestion des votes.
	 *
	 * @param object r
	 *
	 * @return void
	 */
	function _setRatingEvents(r)
	{
		if (!_params.votes)
		{
			return;
		}

		const empty = '\ue9d7', full = '\ue9d9';
		let ajax_active = false, rating = r.items[_currentPosition].user.rating;

		// Suppression du vote.
		function click_delete()
		{
			if (ajax_active)
			{
				return false;
			}
			ajax_active = true;

			App.ajax(
			{
				section: 'vote-remove',
				item_id: _items[_currentPosition].id
			},
			{
				always: r => ajax_active = false,
				dflt: r => console.log(r),
				error: r => _alertError(r.message),
				success: r =>
				{
					App.text('#diaporama_user_note_rating a', empty);
					_q('#diaporama_user_note_delete').style.visibility = 'hidden';
					_q('#diaporama_user_note_rating a:first-child').focus();

					update(r);
					_items[_currentPosition].user.rating = null;

					if (_q('#item') && DIAPORAMA.item_id == ITEM.id)
					{
						Gallery.updateRating?.(r);
					}
				}
			});
		}

		// Ajout du vote.
		function click_rating()
		{
			rating = parseInt(App.attr(this, 'data-rating'));
			if (ajax_active || _items[_currentPosition].user.rating == rating)
			{
				return false;
			}
			ajax_active = true;

			App.ajax(
			{
				section: 'vote-add',
				rating: rating,
				item_id: _items[_currentPosition].id
			},
			{
				always: r => ajax_active = false,
				dflt: r => console.log(r),
				error: r => _alertError(r.message),
				success: r =>
				{
					_q('#diaporama_user_note_delete').style.visibility = 'visible';

					let n = 1;
					App.each('#diaporama_user_note_rating a', elem =>
					{
						App.text(elem, n > rating ? empty : full);
						n++;
					});

					update(r);

					if (_q('#item') && DIAPORAMA.item_id == ITEM.id)
					{
						Gallery.updateRating?.(r, rating);
					}
				}
			});
		}

		// Gestion du survol de la souris.
		function mouse(rating)
		{
			if (!ajax_active)
			{
				let n = 1;
				App.each('#diaporama_user_note_rating a', elem =>
				{
					App.text(elem, n > rating ? empty : full);
					n++;
				});
			}
		}
		function mouseenter()
		{
			mouse(parseInt(App.attr(this, 'data-rating')));
		}
		function mouseleave()
		{
			mouse(_items[_currentPosition].user.rating);
		}

		// Mise à jour des informations.
		function update(r)
		{
			if (_query.match(/\/votes\/?/))
			{
				_getData(false, false, _currentPosition, _currentPosition);
				return;
			}

			// Note courante de l'utilisateur.
			_items[_currentPosition].user.rating = r.user_rating;
			_items[_currentPosition].user.rating_array = r.user_rating_array;

			// Panneau d'informations.
			const stat_rating = _q('#diaporama_sidebar_stats_rating span');
			let html = '';
			for (const val of r.rating_array)
			{
				html += `<span class="rating">${val == 1
					? '&#xe9d9;' : (val == 0 ? '&#xe9d7;' : '&#xe9d8;')}</span>`;
			}
			App.html(stat_rating, html);
			App.attr(stat_rating, 'title', r.rating);
			App.text('#diaporama_sidebar_stats_votes span', r.votes_short);

			// Panneau d'ajout d'une note.
			const note = _q('#diaporama_note_rating');
			const votes_l10n = r.votes_short > 1
				? _l10n.votes_number_multiple
				: _l10n.votes_number;
			App.html(note, html);
			App.attr('#diaporama_note_rating', 'title', r.rating);
			App.text('#diaporama_note_formated', votes_l10n.replace('%s', r.votes_short));
		}

		// Événements.
		App.each('#diaporama_user_note_rating a', elem =>
		{
			App.on(elem, 'mouseenter', mouseenter);
			App.click(elem, click_rating);
		});
		App.on('#diaporama_user_note_rating', 'mouseleave', mouseleave);
		App.click('#diaporama_user_note_delete', click_delete);
	}

	/**
	 * Gestion de la sélection.
	 *
	 * @return void
	 */
	function _setSelectionEvents()
	{
		if (!_icon('selection'))
		{
			return;
		}
		App.click(_icon('selection'), function()
		{
			if (_items[_currentPosition] === undefined)
			{
				return;
			}

			const action = App.hasClass(this, 'active') ? 'remove' : 'add';

			if ((action == 'add' && _items[_currentPosition].in_selection)
			|| ((action == 'remove' && !_items[_currentPosition].in_selection)))
			{
				return;
			}

			App.toggleClass(this, 'active', action == 'add');
			App.toggleClass('#diaporama_selection', 'in', action == 'add');
			App.attr(this, 'title', _l10n[`selection_${action == 'add' ? 'remove' : 'add'}`]);

			App.ajax(
			{
				section: 'selection',
				action: 'selection-' + action,
				id: [_items[_currentPosition].id]
			},
			{
				dflt: r => console.log(r),
				error: r => _alertError(r.message),
				success: r =>
				{
					_items[_currentPosition].in_selection = action == 'add' ? 1 : 0;

					App.each(`.thumbs_items dl[data-id="${_items[_currentPosition].id}"]`, elem =>
					{
						App.html([elem, '.selection_icon'],
							action == 'add' ? '&#xea52;' : '&#xea53;'
						);
						App.toggleClass(elem, 'selected', action == 'add');
					});

					if (typeof Gallery.updateSelection == 'function')
					{
						const item = _q('#item');
						if (!item)
						{
							Gallery.updateSelection(r.stats);
						}
						if (item && DIAPORAMA.item_id == ITEM.id)
						{
							Gallery.updateSelection(action, r);
						}
					}

					if (_query.match(/\/selection\/?/))
					{
						_getData(false, false, _currentPosition, _currentPosition);
					}
				}
			});
		});
	}

	/**
	 * Gestionnaires d'événements pour les panneaux latéraux.
	 *
	 * @return void
	 */
	function _setSidebarsEvents()
	{
		App.each('.diaporama_sidebar_icon', elem =>
		{
			const animate_options =
			{
				duration: _options.sidebarShowDuration,
				easing: 'ease-in-out'
			};
			const name = App.attr(elem, 'data-name');
			const sidebar = _q(`.diaporama_sidebar[data-name="${name}"]`);

			function animate_item(css)
			{
				const item = _q('#diaporama_item_' + _currentPosition);
				if (!item)
				{
					return;
				}
				_options.animate
					? App.animate(item, css, animate_options,
						() => Object.assign(item.style, css))
					: Object.assign(item.style, css);
			}

			function hide(sidebar, callback, animate = true)
			{
				const css = {right: `-${get_width()}px`};
				function finish()
				{
					css.display = 'none';
					Object.assign(sidebar.style, css);
					App.removeClass(_icon(App.attr(sidebar, 'data-name')), 'active');
					callback();
				}
				if (animate)
				{
					_options.animate
						? App.animate(sidebar, css, animate_options, finish)
						: finish();
				}
				else
				{
					finish();
				}
			}

			function get_width()
			{
				const comp = getComputedStyle(sidebar);
				return parseFloat(comp.width == '100%' ? window.innerWidth : comp.width);
			}

			// Affichage du panneau latéral.
			App.click(elem, () =>
			{
				if (_animationActive)
				{
					return;
				}
				_animationActive = true;

				const diaporama_sidebar_width = _options.sidebarItemResize ? get_width() : 0;
				let action;

				// On affiche le panneau latéral.
				if (!sidebar.checkVisibility())
				{
					if (name == 'geolocation' && _items[_currentPosition])
					{
						const is_coords = _items[_currentPosition].geolocation_coords !== '';
						_q('#diaporama_geolocation_map').style.display
							= is_coords ? 'block' : 'none';
						_q('#diaporama_geolocation_coords').style.display
							= is_coords ? 'block' : 'none';
						_q('#diaporama_geolocation_none').style.display
							= is_coords ? 'none' : 'block';
					}
					action = 'show';
					let sidebar_visible = false;
					function show_sidebar(animate = true)
					{
						function after_show()
						{
							Object.assign(sidebar.style, {right: 0});
							_previousDiaporamaSize = _getDiaporamaSize();
							_changeButtonSwitch();
							_animationActive = false;
							App.addClass(_icon(name), 'active');
							if (name == 'geolocation')
							{
								_geolocation(true);
							}
						}
						if (animate && getComputedStyle(sidebar).width != '100%')
						{
							_carouselChangeSizePosition(-diaporama_sidebar_width);
							_changeCenterSizePosition(-diaporama_sidebar_width);
							animate_item(_changeItemSizePosition(_currentPosition,
								true, true, true, -diaporama_sidebar_width));
						}
						sidebar.style.right = `-${get_width()}px`;
						App.show(sidebar);
						if (animate)
						{
							_options.animate
								? App.animate(sidebar, {right: 0}, animate_options, after_show)
								: after_show();
						}
						else
						{
							after_show();
						}
					}

					// Si un panneau latéral est déjà affiché,
					// on le cache avant d'afficher le panneau courant.
					App.each('.diaporama_sidebar', elem =>
					{
						if (elem.checkVisibility())
						{
							hide(elem, () => show_sidebar(false), false);
							sidebar_visible = true;
						}
					});

					if (!sidebar_visible)
					{
						show_sidebar();
					}
				}

				// On cache le panneau latéral.
				else
				{
					action = 'hide';
					const css = _changeItemSizePosition(_currentPosition,
						true, true, true, diaporama_sidebar_width);
					function after_hide()
					{
						_previousDiaporamaSize = _getDiaporamaSize();
						_changeButtonSwitch();
						_animationActive = false;
					}
					_carouselChangeSizePosition(diaporama_sidebar_width);
					_changeCenterSizePosition(diaporama_sidebar_width);
					animate_item(css);

					hide(sidebar, after_hide);
				}

				// Div pour les icônes de favori et de sélection.
				App.each('#diaporama_favorite,#diaporama_selection', elem =>
				{
					const width = action == 'show' ? diaporama_sidebar_width : 0;
					const css = {left: (window.innerWidth - width) + 'px'};
					_options.animate
						? App.animate(elem, css, animate_options,
							() => Object.assign(elem.style, css))
						: Object.assign(elem.style, css);
				});
			});
		});

		// Bouton de fermeture des panneaux latéraux.
		App.click('.diaporama_sidebar_close', function()
		{
			_q('.diaporama_sidebar_icon[data-name="' +
				App.attr(this.closest('section'), 'data-name') + '"]').click();
		});

		// Édition : enregistrement des modifications.
		App.on('.diaporama_sidebar[data-name="edit"] form', 'submit', function(evt)
		{
			evt.preventDefault();

			// Titre et nom de fichier obligatoires.
			if (App.val('#diaporama_edit_title').trim() == ''
			 || App.val('#diaporama_edit_filename').trim() == '')
			{
				return;
			}

			const submit = _q(this, 'input[type="submit"]');
			submit.disabled = true;
			App.after(submit, '<span id="diaporama_edit_loading"></span>');

			const edit = _q('.diaporama_sidebar[data-name="edit"]');
			function edit_report(type, text)
			{
				App.remove('#diaporama_edit_loading');

				const icon = type == 'info'
					? '&#xf06a;'
					: (type == 'error' ? '&#xe904;' : '&#xe905;');
				App.append([edit, '.diaporama_buttons'],
					`<div id="diaporama_report"><span><span>${icon}</span></span></div>`
				);

				const report = _q('#diaporama_report');
				report.style.opacity = 0;
				App.addClass('#diaporama_report > span', `diaporama_report_${type}`);

				const options = {duration: 250, fill: 'forwards'};
				App.animate(submit, {opacity: 0}, options);
				App.afterText([report, 'span span'], text);
				App.animate(report, {opacity: 1}, options, () =>
				{
					setTimeout(() =>
					{
						App.animate(report, {opacity: 0}, options, () => report.remove());
						App.each([edit, '.diaporama_buttons *'], elem =>
						{
							App.animate(elem, {opacity: 1}, options, () =>
							{
								submit.disabled = false;
								submit.focus();
							});
						});
					}, type == 'error' ? 6000 : 3000);
				});
			}

			_keyboardActive = true;

			function success(r)
			{
				edit_report(r.status, r.message);

				// Mise à jour des informations sur la page du fichier.
				if (_q('#item') && DIAPORAMA.item_id == ITEM.id)
				{
					Gallery.updateItem(r, true);
				}

				// Fil d'Ariane.
				const breadcrumb = _q('#diaporama_breadcrumb_item a');
				App.text(breadcrumb, r.title);
				App.attr(breadcrumb, 'href',
					Gallery.changeUrlname(
						breadcrumb.href, 'item',
						_items[_currentPosition].id, r.urlname
					));

				// Titre.
				App.val('#diaporama_edit_title', r.title);
				_items[_currentPosition].title = r.title;

				// Nom de fichier.
				if (_items[_currentPosition].filename != r.filename)
				{
					App.val('#diaporama_edit_filename', r.filename);
					App.attr('#diaporama_tools a[data-name="download"]', 'href', r.download);
					const item = _q('#diaporama_item_' + _currentPosition);
					if (r.is_video)
					{
						item.pause();
						App.attr([item, 'source'], 'src', r.source);
						item.load();
					}
					else
					{
						App.attr(item, 'src', r.source);
					}
					_items[_currentPosition].filename = r.filename;
				}

				// Description.
				App.val('#diaporama_edit_desc', r.description);
				const description = _q('#diaporama_sidebar_desc');
				if (r.description === null)
				{
					App.hide(description);
				}
				else
				{
					App.html([description, '.diaporama_sidebar_content'], r.description_formated);
					App.show(description);
				}
				_items[_currentPosition].description = r.description;
				_items[_currentPosition].description_formated = r.description_formated;

				// Tags.
				if (r.tags_list)
				{
					App.val('#diaporama_edit_tags', r.tags_list.join(', '));
					_tags(r);
				}

				// Titre et description sur l'image.
				_changeOverImageTextDisplay();
			}

			App.ajax(
			{
				section: 'item-edit',
				item_id: _items[_currentPosition].id,
				title: App.val('#diaporama_edit_title'),
				filename: App.val('#diaporama_edit_filename'),
				description: App.val('#diaporama_edit_desc'),
				tags: App.val('#diaporama_edit_tags')
			},
			{
				dflt: () => edit_report('error', 'Unknown error.'),
				error: r => edit_report(r.status, r.message),
				info: success,
				success: success
			},
			r =>
			{
				edit_report('error', 'PHP error.');
				console.log(r.responseText);
			});
		});
	}

	/**
	 * Gestion du swipe.
	 *
	 * @return void
	 */
	function _setSwipeEvents()
	{
		if (!_options.swipe)
		{
			return;
		}

		const inner = _q('#diaporama_inner');

		function css(elem, css, callback)
		{
			if (!elem)
			{
				return;
			}
			const options =
			{
				duration: _options.animate ? parseInt(_options.swipeDuration) : 0,
				easing: 'ease-in-out'
			};
			App.animate(elem, css, options, () =>
			{
				Object.assign(elem.style, css);
				callback?.();
			});
		}

		function start(evt)
		{
			evt.preventDefault();
			if (!App.hasClass('#diaporama_switch', 'fullsize')
			&& (evt.button === 0 || evt.button === undefined)
			&& !_animationActive && !_switchActive)
			{
				swipe(evt);
			}
		}

		function swipe(evt)
		{
			const s = _getDiaporamaSize();
			const start = evt.changedTouches ? evt.changedTouches[0] : evt;
			let move_left = 0;

			// Fichier courant.
			const item_current = _q('#diaporama_item_' + _currentPosition);
			if (!item_current)
			{
				return;
			}
			_animationActive = true;
			_realsize = false;
			_zoom = {};
			_changeItemSizePosition(_currentPosition);
			_swipeActive = true;
			const item_current_left = parseFloat(getComputedStyle(item_current).left);
			App.each('#diaporama video', elem => elem.pause());

			if (!_options.clickZoom)
			{
				_changeCursor('grab');
			}

			// Fichier précédent.
			const item_prev = _q('#diaporama_item_' + (_currentPosition - 1));
			let item_prev_left = 0, item_prev_left_initial = 0;
			if (item_prev)
			{
				_changeItemSizePosition(_currentPosition - 1);
				item_prev_left_initial = parseFloat(getComputedStyle(item_prev).left);
				item_prev_left = item_prev_left_initial - s.availableWidth;
				Object.assign(item_prev.style, {display: 'block', left: item_prev_left + 'px'});
			}

			// Fichier suivant.
			const item_next = _q('#diaporama_item_' + (_currentPosition + 1));
			let item_next_left = 0, item_next_left_initial = 0;
			if (item_next)
			{
				_changeItemSizePosition(_currentPosition + 1);
				item_next_left_initial = parseFloat(getComputedStyle(item_next).left);
				item_next_left = item_next_left_initial + s.availableWidth;
				Object.assign(item_next.style, {display: 'block', left: item_next_left + 'px'});
			}

			function auto()
			{
				if (_autoActive)
				{
					clearTimeout(_autoTimer);
					_autoTimer = setTimeout(_autoChangeItem, _options.autoDuration * 1000);
				}
			}

			function change()
			{
				auto();
				_animationActive = false;
				_swipeActive = false;
				_getData(false, false, _currentPosition, _currentPosition);
				_changeItemInfos();
				_changeButtonsNavigation();
				_changeButtonSwitch();
			}

			function move(evt)
			{
				_changeCursor('grab');
				const move = evt.changedTouches ? evt.changedTouches[0] : evt;
				move_left = start.pageX - move.pageX;
				item_current.style.left = (item_current_left - move_left) + 'px';
				if (item_prev)
				{
					item_prev.style.left = (item_prev_left - move_left) + 'px';
				}
				if (item_next)
				{
					item_next.style.left = (item_next_left - move_left) + 'px';
				}
			}

			function end(evt)
			{
				App.off(document, {'mousemove': move, 'mouseup': end});
				App.off(inner, {'touchmove': move, 'touchend': end});

				if (!_options.clickZoom)
				{
					_changeCursor('default');
				}

				// Déplacement vers la gauche.
				if (move_left > _options.swipeDistance && item_next)
				{
					css(item_prev,
					{
						left: (item_prev_left_initial - (s.availableWidth * 2)) + 'px'
					});
					css(item_current, {left: (item_current_left - s.availableWidth) + 'px'}, () =>
					{
						if (item_prev)
						{
							App.hide(item_prev);
						}
						if (item_current)
						{
							App.hide(item_current);
						}
						_currentPosition++;
						change();
					});
					css(item_next, {left: item_next_left_initial + 'px'});
					return;
				}

				// Déplacement vers la droite.
				if (move_left < -_options.swipeDistance && item_prev)
				{
					css(item_prev, {left: item_prev_left_initial + 'px'});
					css(item_current, {left: (item_current_left + s.availableWidth) + 'px'}, () =>
					{
						if (item_next)
						{
							App.hide(item_next);
						}
						if (item_current)
						{
							App.hide(item_current);
						}
						_currentPosition--;
						change();
					});
					css(item_next,
					{
						left: (item_next_left_initial + (s.availableWidth * 2)) + 'px'
					});
					return;
				}

				// Aucun déplacement : on reste sur le fichier courant.
				if (parseFloat(item_current.style.left) == item_current_left
				&& _options.clickZoom)
				{
					auto();
					_animationActive = false;
					_swipeActive = false;
					_clickZoom(evt);
					App.hide(item_prev);
					App.hide(item_next);
				}
				else
				{
					css(item_current, {left: item_current_left + 'px'}, () =>
					{
						auto();
						_animationActive = false;
						_swipeActive = false;
						_changeButtonSwitch();
					});
					if (item_prev)
					{
						css(item_prev, {left: item_prev_left + 'px'}, () => App.hide(item_prev));
					}
					if (item_next)
					{
						css(item_next, {left: item_next_left + 'px'}, () => App.hide(item_next));
					}
				}
			}

			App.on(document, {'mousemove': move, 'mouseup': end});
			App.on(inner, {'touchmove': move, 'touchend': end});
		}

		App.on(inner, {'mousedown': start, 'touchstart': start});
	}

	/**
	 * Gestion du zoom à la souris.
	 *
	 * @return void
	 */
	function _setZoomEvents()
	{
		App.on('#diaporama_inner', 'wheel', evt =>
		{
			evt.preventDefault();

			_zoom = {};

			// Pas de zoom si une animation est en cours,
			// si l'option de zoom est désactivée ou si le diaporama est fermé.
			if (_animationActive || !_options.zoom || !_q('#diaporama').checkVisibility())
			{
				return;
			}

			// Pas de zoom pour les vidéos ?
			if (_items[_currentPosition].is_video && !_options.zoomVideo)
			{
				return;
			}

			// Application du zoom.
			_zoomItem(evt, false, _options.zoomLimit);
		});
	}

	/**
	 * Gestion de l'affichage des icônes des panneaux latéraux.
	 *
	 * @return void
	 */
	function _sidebarsIcons()
	{
		const icons =
		{
			edit: _items[_currentPosition].user.edit,
			geolocation: _params.geolocation,
			votes: _params.votes
		};
		for (const [name, display] of Object.entries(icons))
		{
			if (window.innerWidth > _options.mobileMaxWidth)
			{
				_icon(name).style.display = display ? 'inline' : 'none';
			}
			else
			{
				if (App.hasClass(_icon(name), 'active'))
				{
					_icon(name).click();
				}
				App.hide(_icon(name));
			}
		}
	}

	/**
	 * Génère la liste des tags dans le panneau d'informations.
	 *
	 * @param object data
	 *
	 * @return void
	 */
	function _tags(data)
	{
		const tags = _q('#diaporama_sidebar_tags');
		if (data.tags.length)
		{
			const tags_content = _q(tags, '.diaporama_sidebar_content');
			App.html(tags_content, document.createElement('ul'));
			for (const tag in data.tags)
			{
				const li = document.createElement('li');
				const a = document.createElement('a');
				App.attr(a, 'href', data.tags[tag].link);
				App.text(a, data.tags[tag].name);
				li.append(a);
				_q(tags_content, 'ul').append(li);
			}
			App.show(tags);
		}
		else
		{
			App.hide(tags);
		}
	}

	/**
	 * Zoom sur une image/vidéo.
	 *
	 * @param object evt
	 * @param bool click
	 * @param int limit
	 *
	 * @return void
	 */
	function _zoomItem(evt, click, limit)
	{
		const item = _q('#diaporama_item_' + _currentPosition);
		const round = o => { for (const n in o) { o[n] = Math.round(o[n]); } };
		const s = _getDiaporamaSize();
		const style = prop => parseFloat(getComputedStyle(item)[prop]);

		// Nouvelle taille ("click").
		if (click)
		{
			if (item.style.cursor == 'zoom-out')
			{
				_zoom.width = 1;
				_zoom.height = 1;
			}
			else
			{
				_zoom.width = style('width') * 2;
				_zoom.height = style('height') * 2;
				if (((_zoom.width / _items[_currentPosition].width_resized) * 100) > 65)
				{
					_zoom.width = _items[_currentPosition].width_resized;
					_zoom.height = _items[_currentPosition].height_resized;
				}
			}
		}

		// Nouvelle taille ("wheel").
		else
		{
			if (evt.deltaY < 0)
			{
				_zoom.width = style('width') * _options.zoomFactor;
				_zoom.height = style('height') * _options.zoomFactor;
			}
			else
			{
				_zoom.width = style('width') / _options.zoomFactor;
				_zoom.height = style('height') / _options.zoomFactor;
			}
		}

		// Limitation du zoom.
		if (limit == 100
		&& (_zoom.width > _items[_currentPosition].width_resized
		|| _zoom.height > _items[_currentPosition].height_resized))
		{
			_realsize = true;
			_zoom.width = _items[_currentPosition].width_resized;
			_zoom.height = _items[_currentPosition].height_resized;
		}
		if (limit > 0)
		{
			if (((_zoom.width / _items[_currentPosition].width_resized) * 100) > limit)
			{
				_zoom.width = _items[_currentPosition].width_resized * (limit / 100);
				_zoom.height = _items[_currentPosition].height_resized * (limit / 100);
			}
		}

		round(_zoom);

		// On évite d'avoir une taille plus petite
		// que la taille redimensionnée initiale.
		if (_zoom.width <= App.attr(item, 'data-contain-width')
		 || _zoom.height <= App.attr(item, 'data-contain-height'))
		{
			if (click)
			{
				_q('#diaporama_switch').click();
			}
			else
			{
				_zoom = {};
				_realsize = false;
				_changeItemSizePosition(_currentPosition, false);
			}
			return;
		}

		// Position.
		const add_height = click ? (s.availableHeight / 2) - (evt.clientY - s.barTopHeight) : 0;
		const add_width = click ? (s.availableWidth / 2) - evt.clientX : 0;
		const diff_height = _zoom.height - style('height');
		const diff_width = _zoom.width - style('width');
		let pos_ratio_height = 0.5;
		let pos_ratio_width = 0.5;
		pos_ratio_width = 1 / (style('width') / (evt.clientX - style('left')));
		pos_ratio_height = 1 / (style('height') / (evt.clientY - style('top')));
		_zoom.top = style('top') - (diff_height * pos_ratio_height) + add_height;
		_zoom.left = style('left') - (diff_width * pos_ratio_width) + add_width;

		// Correction de la position (horizontale).
		if (_zoom.width < s.availableWidth)
		{
			_zoom.left = (s.availableWidth - _zoom.width) / 2;
		}
		else if ((_zoom.left + _zoom.width) < s.availableWidth)
		{
			_zoom.left += s.availableWidth - (_zoom.left + _zoom.width);
		}
		else if (_zoom.left > 0)
		{
			_zoom.left = 0;
		}

		// Correction de la position (verticale).
		if (_zoom.height < s.availableHeight)
		{
			_zoom.top = ((s.availableHeight - _zoom.height) / 2) + s.barTopHeight;
		}
		else if ((_zoom.top + _zoom.height - s.barTopHeight) < s.availableHeight)
		{
			_zoom.top += s.availableHeight - (_zoom.top + _zoom.height - s.barTopHeight);
		}
		else if (_zoom.top > s.barTopHeight)
		{
			_zoom.top = s.barTopHeight;
		}

		round(_zoom);

		// Application des règles CSS.
		const css =
		{
			top: _zoom.top + 'px',
			left: _zoom.left + 'px',
			width: _zoom.width + 'px',
			height: _zoom.height + 'px'
		};
		const after_animation = () =>
		{
			Object.assign(item.style, css);
			_animationActive = false;
			_changeButtonSwitch();
			if (click)
			{
				_zoomText();
			}
		};
		_animationActive = true;
		if (click && _options.animate)
		{
			App.animate(item, css,
			{
				duration: _options.itemResizeDuration,
				easing: 'ease-in-out'
			},
			after_animation);
		}
		else
		{
			after_animation();
		}
	}

	/**
	 * Gestion de l'affichage du niveau de zoom sur mobile.
	 *
	 * @return void
	 */
	function _zoomText()
	{
		if (window.innerWidth > 700)
		{
			return;
		}

		const item = _q('#diaporama_item_' + _currentPosition);
		const pc = Math.round((item.offsetWidth / _items[_currentPosition].width_resized) * 100);

		App.text('#diaporama_position span', `${pc}%`);
		clearTimeout(_zoomTextTimer);
		_zoomTextTimer = setTimeout(_changePosition, 2000);
	}
}