'use strict';

/**
 * Gestion de la galerie.
 *
 * @license https://www.gnu.org/licenses/gpl-3.0.html
 * @link https://www.igalerie.org/
 */
const Gallery = new function()
{
	// Raccourcis.
	const _q = App.q, _qAll = App.qAll;



	// Objet "Boxes".
	this.boxes;

	// Objet "BoxesArrow".
	this.boxesArrow;

	// Objet "Diaporama".
	this.diaporama;

	// L'écran est-il de petite taille ?
	this.isSmallScreen = () => window.innerWidth < 1180;

	// Fonction de mise à jour des favoris.
	this.updateFavorites;

	// Fonction de mise à jour des votes.
	this.updateRating;

	// Fonction de mise à jour de la sélection.
	this.updateSelection;



	/**
	 * Initialisation.
	 *
	 * @return void
	 */
	App.ready(() =>
	{
		// Permet d'indiquer à CSS que JavaScript est activé.
		App.replaceClass('html', 'nojs', 'js');

		// Thème.
		if (typeof Theme == 'function')
		{
			Theme();
		}

		// Boîtes flottantes.
		_boxes();

		// Diaporama.
		_diaporama();

		// Favoris.
		_favorites();

		// Formulaires.
		_form();

		// Langues.
		_langs();

		// Déconnexion.
		_logout();

		// Photo ou vidéo au hasard.
		_randomItem();

		// Votes.
		_rating();

		// Recherche.
		_search();

		// Sélection.
		_selection();

		// Tableaux.
		_table();

		// Ajout de fichiers.
		_upload();

		// Vidéos.
		_video();

		// Visionneuse d'image.
		_viewer();

		// Géolocalisation.
		_worldmap();
	});



	/**
	 * Modifie le nom d'URL d'un objet dans une URL complète.
	 *
	 * @param string url
	 * @param string type
	 * @param integer id
	 * @param string urlname
	 *
	 * @return string
	 */
	this.changeUrlname = function(url, type, id, urlname)
	{
		const regexp = new RegExp(`(\\W(?:${type})/${id}\\-)[^/#]+`, 'i');
		return url.replace(regexp, `$1${urlname}`);
	};

	/**
	 * Recharge le diaporama sur la page des fichiers.
	 *
	 * @return void
	 */
	this.reloadDiaporama = function()
	{
		if (DIAPORAMA.item_id == ITEM.id)
		{
			Gallery.diaporama = null;
		}
	};

	/**
	 * Ajoute un bouton permettant de visionner un champ de mot de passe en clair.
	 *
	 * @param object elem
	 *
	 * @return void
	 */
	this.passwordViewer = function(elem)
	{
		App.wrap(elem, App.createElement('<span class="password_container"></span>'));

		const view = App.createElement(
			'<span class="password_view">' +
				'<a href="javascript:;">&#xe9ce;</a>' +
			'</span>'
		);
		App.click(view, () =>
		{
			const input = _q(view.closest('.password_container'), 'input');
			const is_text = input.type == 'text';
			App.attr(input, 'type', is_text ? 'password' : 'text');
			App.html([view, 'a'], is_text ? '&#xe9ce;' : '&#xe9d1;');
			input.focus();
		});
		App.after(elem, view);

		if (elem.autofocus)
		{
			elem.focus();
		}
	};

	/**
	 * Mise à jour des informations d'un fichier sur la page des fichiers.
	 *
	 * @param object r
	 * @param boolean diaporama
	 *
	 * @return void
	 */
	this.updateItem = (r, diaporama) =>
	{
		if (r.status == 'success' && !diaporama)
		{
			Gallery.reloadDiaporama();
		}

		// Titre.
		if (r.title !== undefined)
		{
			BOX_ITEM.form.title = r.title;
			App.val('#box_edit_item_title', r.title);
			App.text('#item_title', r.title);
			App.text('title', r.page_title);
			if (_q('#item_title'))
			{
				const url = new URL(window.location);
				window.history.pushState({}, '',
					Gallery.changeUrlname(url.href, 'item', ITEM.id, r.urlname));
			}
		}

		// Nom de fichier.
		if (r.filename !== undefined)
		{
			BOX_ITEM.form.filename = r.filename;
			App.val('#box_edit_item_filename', r.filename);
			App.attr('#link_download a', 'href', r.download);
			if (App.hasClass('#item_link', 'viewer'))
			{
				App.attr('#item_link', 'href', r.source);
			}
			if (r.is_video)
			{
				_q('#item video').pause();
				App.attr('#item source', 'src', r.source);
				_q('#item video').load();
			}
			else
			{
				App.attr('#item img', 'src', r.source);
			}
		}

		// Description.
		if (r.description !== undefined)
		{
			BOX_ITEM.form.description = r.description;
			App.val('#box_edit_item_description', r.description);
			App.remove('#item_description');
			if (r.description !== null)
			{
				App.before('#item_columns',
					'<div id="item_description" class="object_desc">' +
						r.description_formated +
					'</div>'
				);
			}
		}

		// Tags.
		if (r.tags_list !== undefined)
		{
			const tags_list = r.tags_list.join(', ');
			BOX_ITEM.form.tags = tags_list;
			App.val('#box_edit_item_tags', tags_list);
		}
		if (r.tags !== undefined)
		{
			App.remove('#item_tags');
			if (r.tags.length)
			{
				const item_tags = App.createElement(
					'<div id="item_tags">' +
						'<h3 class="icon"><span>&#xf02c;</span> </h3>' +
						'<ul></ul>' +
					'</div>'
				);
				App.appendText([item_tags, 'h3'], BOX_ITEM.l10n.tags);

				for (const tag of r.tags)
				{
					const li = App.createElement('<li><a></a></li>');
					App.attr([li, 'a'], 'href', tag.link);
					App.text([li, 'a'], tag.name);
					App.append([item_tags, 'ul'], li);
				}

				App.append('#item_left_inner', item_tags);
			}
		}
	};



	/**
	 * Gestion des boîtes flottantes.
	 *
	 * @return void
	 */
	function _boxes()
	{
		// Boîtes flottantes.
		Gallery.boxes = new Boxes();
		App.on('[data-box]', 'click', Gallery.boxes.open);

		// Boîtes fléchées et menu principal.
		Gallery.boxesArrow = new BoxesArrow();
	}

	/**
	 * Gestion du diaporama.
	 *
	 * @return void
	 */
	function _diaporama()
	{
		// Lien de lancement du diaporama.
		if (_q('#link_diaporama'))
		{
			const link = _q('#link_diaporama a');

			App.click('#link_diaporama a', () =>
			{
				if (!Gallery.diaporama)
				{
					Gallery.diaporama = new Diaporama(DIAPORAMA.query, link);
				}
				Gallery.diaporama.start(DIAPORAMA.position, DIAPORAMA.options);
			});

			// Démarrage automatique du diaporama au chargement de la page.
			if (new URL(window.location).hash == '#diapo')
			{
				link.click();
			}
		}
	}

	/**
	 * Ajout ou retrait d'un fichier dans les favoris.
	 *
	 * @return void
	 */
	function _favorites()
	{
		let ajax;

		if (_q('#item') && _q('#link_favorites'))
		{
			Gallery.updateFavorites = (action, r) =>
			{
				App.toggleClass('#item', 'favorite', action == 'add');
				App.toggleClass('#link_favorites', 'add', action != 'add');
				App.toggleClass('#link_favorites', 'remove', action == 'add');
				App.attr('#link_favorites a', 'title', FAVORITES.l10n[action]);
				App.text('#item_favorites span:first-child', r.favorites_short);
			};

			App.click('#link_favorites a', () =>
			{
				if (ajax)
				{
					return;
				}

				const action = App.hasClass('#item', 'favorite') ? 'remove' : 'add';

				ajax = App.ajax(
				{
					section: 'favorites-' + action,
					item_id: ITEM.id
				},
				{
					always: r => setTimeout(() => ajax = null, 500),
					error: r => console.log('error: ' + r.message),
					success: r =>
					{
						Gallery.updateFavorites(action, r);
						if (GALLERY.q_pageless.match(/\/(user-)?favorites(\/\d+){1,2}$/)
						&& action == 'remove')
						{
							window.location = ITEM.link;
						}
						Gallery.reloadDiaporama();
					}
				});
			});
		}
	}

	/**
	 * Gestion des formulaires.
	 *
	 * @return void
	 */
	function _form()
	{
		// Ajoute le nom du fichier sélectionné dans l'élément <label>
		// rattaché à un élément <input type="file">.
		App.on('input[type="file"]', 'change', function(evt)
		{
			const label = _q(`#${this.id} + label`);
			if (label && evt.target.value)
			{
				App.text(label, evt.target.value.replace(/^.*?([^/\\]+)$/, '$1'));
			}
		});

		// Vérification du poids du fichier avant envoi du formulaire.
		App.on('form[data-l10n-maxsize]', 'submit', function(evt)
		{
			if (_q(this, 'input[type="file"]').files.item(0).size
			> parseInt(App.attr(this, 'data-maxsize')))
			{
				alert(App.attr(this, 'data-l10n-maxsize'));
				evt.preventDefault();
			}
		});

		// Demande de confirmation avant envoi du formulaire.
		App.on('form[data-l10n-confirm]', 'submit', function(evt)
		{
			if (!confirm(App.attr(this, 'data-l10n-confirm')))
			{
				evt.preventDefault();
			}
		});

		// Permet d'éviter l'envoi du formulaire de navigation
		// entre les pages si la valeur de la page entrée n'est
		// pas valide ou identique à la page courante.
		App.on('.pages form', 'submit', function(evt)
		{
			const page = _q(this, 'input[name="page"]');
			if (!page.value.match(/^[1-9]\d{0,11}$/) || page.value == page.defaultValue
			 || parseInt(page.value) > parseInt(App.text([this, '.count'])))
			{
				evt.preventDefault();
			}
		});

		// Visionneur de mot de passe.
		App.each('input[data-password-view]', Gallery.passwordViewer);

		// Ajout de la date actuelle.
		App.click('.dt_now', function()
		{
			const dt = new Date();
			const now = (sel, val) =>
			{
				const input = _q(this.closest('p'), sel);
				if (input)
				{
					input.value = sel == '.dt_year' ? val : ('0' + val).slice(-2);
				}
			};
			now('.dt_year', dt.getFullYear());
			now('.dt_month', dt.getMonth() + 1);
			now('.dt_day', dt.getDate());
			now('.dt_hour', dt.getHours());
			now('.dt_minute', dt.getMinutes());
			now('.dt_second', dt.getSeconds());
		});

		// Suppression de la date.
		App.click('.dt_reset', function()
		{
			App.val([this.closest('p'), 'input'], '');
		});
	}

	/**
	 * Menu des langues.
	 *
	 * @return void
	 */
	function _langs()
	{
		if (_q('#footer_lang'))
		{
			function close(evt)
			{
				if (!evt || evt.target == this)
				{
					App.off(window, 'keyup', keyup);
					App.remove('#lang_switch');
				}
			}
			function keyup(evt)
			{
				if (evt.keyCode == 27)
				{
					close();
				}
			}
			App.click('#footer_lang a', () =>
			{
				const div = App.createElement('<div id="lang_switch"><ul></ul></div>');
				App.on(div, 'click', close);

				for (const lang of LANG_SWITCH)
				{
					const current = lang.code == _q('html').lang ? ' class="current"' : '';
					const li = App.createElement(
						`<li${current}>` +
							'<a href="javascript:;"><span></span></a>' +
						'</li>'
					);
					App.appendText([li, 'a'], lang.name);
					App.attr([li, 'a'], 'data-code', lang.code);
					App.text([li, 'span'], `[${lang.code}]`);

					App.click([li, 'a'], function()
					{
						const form = App.createElement(
							'<form id="form_lang" method="post">' +
								'<input name="anticsrf" type="hidden">' +
								'<input name="lang" type="hidden">' +
							'</form>'
						);
						App.val([form, 'input[name="anticsrf"]'], GALLERY.anticsrf);
						App.val([form, 'input[name="lang"]'], App.attr(this, 'data-code'));

						App.prepend('main', form);
						_q('#form_lang').submit();
					});

					App.append([div, 'ul'], li);
				}

				App.prepend('main', div);
				App.on(window, 'keyup', keyup);
			});
		}
	}

	/**
	 * Déconnexion d'un compte utilisateur ou d'une catégorie.
	 *
	 * @return void
	 */
	function _logout()
	{
		App.click('#user_logout a, #link_logout a', function()
		{
			App.ajax(
			{
				section: 'logout',
				id: App.attr(this, 'data-id') || 0,
				from_admin: 0
			},
			{
				error: r => alert('error: ' + r.message),
				success: r => window.location = r.redirect
			});
		});
	}

	/**
	 * Gestion de la photo ou vidéo au hasard.
	 *
	 * @return void
	 */
	function _randomItem()
	{
		const random = _q('#bottom_random');
		if (!random)
		{
			return;
		}

		const duration = 250;
		let data, is_ajax = false, is_animate = false;

		function change_thumb()
		{
			App.attr([random, 'dl'], 'data-id', data.item.id);

			// Sélection.
			App.toggleClass([random, 'dl'], 'selected', data.item.in_selection);
			App.html([random, '.selection_icon'],
				data.item.in_selection ? '&#xea52;' : '&#xea53;');

			// Lien.
			App.attr([random, 'dt a'], 'href', data.item.link_item + '#content');

			// Vidéo.
			App.toggleClass([random, 'dl'], 'video', data.item.is_video);
			App.remove([random, '.duration']);
			if (data.item.is_video)
			{
				const span = App.createElement('<span class="duration"></span>');
				App.attr(span, 'id', 'videoduration_' + data.item.id);
				App.text(span, data.item.duration_text);
				App.prepend([random, 'dt a'], span);
			}

			// Nouveau ?
			App.remove([random, '.new']);
			if (data.item.is_recent)
			{
				const span = App.createElement('<span class="new"></span>');
				App.text(span, data.l10n.new);
				App.prepend([random, 'dt a'], span);
			}

			// Statistiques.
			['comments', 'favorites', 'views', 'votes'].forEach(stat =>
			{
				App.text([random, `.${stat} span:first-child`], data.item.stats[stat]);
			});

			// Image.
			App.attr([random, 'img'], 'src', data.item.thumb_src);
			App.attr([random, 'img'], 'alt', data.item.title);

			// Titre.
			App.attr([random, 'dd a'], 'href', data.item.link_item);
			App.text([random, 'dd a'], data.item.title);

			// On fait réapparaître la vignette.
			const show = () =>
			{
				_q(random, 'dl').style.visibility = 'visible';
				App.animate([random, 'dl'], {opacity: 1}, duration, () =>
				{
					_q(random, 'dl').style.opacity = 1;
				});
				App.replaceClass([random, 'h3 a'], 'load', 'finish');
			};
			const img = new Image();
			img.onerror = show;
			img.onload = show;
			img.src = data.item.thumb_src;
		}

		App.append([random, 'h3'], '<a href="javascript:;">&#xe984;</a>');

		App.click([random, 'h3 a'], function()
		{
			if (App.hasClass(this, 'load') || App.hasClass(this, 'finish'))
			{
				return;
			}

			App.addClass(this, 'load');

			is_ajax = true;
			App.ajax(
			{
				section: 'random-item',
				item_size: GALLERY.thumbs_items_size,
			},
			{
				success: r =>
				{
					data = r;
					new Image().src = r.item.thumb_src;
					is_ajax = false;
					if (!is_animate)
					{
						change_thumb();
					}
				}
			});

			is_animate = true;
			App.animate([random, 'dl'], {opacity: 0}, duration, () =>
			{
				App.style([random, 'dl'], {opacity: 0, visibility: 'hidden'});
				is_animate = false;
				if (!is_ajax)
				{
					change_thumb();
				}
			});
		});

		App.on([random, 'h3 a'], 'transitionend', function()
		{
			App.removeClass(this, 'finish');
		});
	}

	/**
	 * Gestion des votes.
	 *
	 * @return void
	 */
	function _rating()
	{
		if (!_q('#item_user_note'))
		{
			return;
		}

		const empty = '\ue9d7', full = '\ue9d9', half = '\ue9d8';

		let ajax, rating_current = [];

		function reload()
		{
			Gallery.reloadDiaporama();

			// Si on se trouve dans la section des fichiers les mieux notés,
			// on recharge la page car le fichier ne se trouve plus forcément
			// à la même position dans cette section, ce qui ne chargerait
			// pas la bonne image au lancement du diaporama.
			if (/\/votes\/\d+$/.test(GALLERY.q_pageless))
			{
				App.pageReload();
			}
		}

		Gallery.updateRating = (r, rating) =>
		{
			App.attr('#item_user_note', 'data-rating', r.delete ? 0 : rating);
			App.toggleClass('#item_user_note_delete', 'hidden', r.delete);

			if (r.delete)
			{
				App.text('#item_user_note .rating', empty);
			}
			else
			{
				let n = 1;
				App.each('#item_user_note .rating', elem =>
				{
					App.text(elem, n > rating ? empty : full);
					n++;
				});
			}

			for (let i = 0; i < r.rating_array.length; i++)
			{
				App.text(`#item_note span[data-rating="${i + 1}"]`,
					r.rating_array[i] == 1 ? full : (r.rating_array[i] == 0 ? empty : half));
			}

			App.text('#item_rating_votes', r.votes_short);
			App.text('#item_note_formated', r.rating);

			rating_current = [];
			App.each('#item_user_note .rating', elem =>
			{
				rating_current.push(App.text(elem));
			});
		}

		// Initialisation.
		App.each('#item_user_note .rating', elem => rating_current.push(App.text(elem)));

		// Gestion du survol de la souris.
		App.on('#item_user_note .rating', 'mouseenter', function()
		{
			if (!ajax)
			{
				const rating = App.attr(this, 'data-rating');
				let n = 1;
				App.each('#item_user_note .rating', elem =>
				{
					elem.textContent = n > rating ? empty : full;
					n++;
				});
			}
		});
		App.on('#item_user_note > span', 'mouseleave', () =>
		{
			if (!ajax)
			{
				let i = 0;
				App.each('#item_user_note .rating', elem =>
				{
					elem.textContent = rating_current[i];
					i++;
				});
			}
		});

		// Ajout d'une note.
		App.click('#item_user_note .rating', function()
		{
			const rating = App.attr(this, 'data-rating');
			if (ajax || App.attr('#item_user_note', 'data-rating') == rating)
			{
				return;
			}
			ajax = App.ajax(
			{
				section: 'vote-add',
				rating: rating,
				item_id: ITEM.id
			},
			{
				always: r => ajax = null,
				error: r => alert('error: ' + r.message),
				success: r =>
				{
					Gallery.updateRating(r, rating);
					reload();
				}
			});
		});

		// Suppression de la note.
		App.click('#item_user_note_delete', () =>
		{
			if (ajax)
			{
				return;
			}
			ajax = App.ajax(
			{
				section: 'vote-remove',
				item_id: ITEM.id
			},
			{
				always: r => ajax = null,
				error: r => alert('error: ' + r.message),
				success: r =>
				{
					Gallery.updateRating(r);
					reload();
					_q('#item_user_note > span a[data-rating="1"]').focus();
				}
			});
		});
	}

	/**
	 * Gestion du moteur de recherche.
	 *
	 * @return void
	 */
	function _search()
	{
		// Affichage des options.
		App.click('#search_options_link a', () =>
		{
			const options = _q('#search_options');
			options.style.display = options.checkVisibility() ? 'none' : 'block';
			App.html('#search_options_link span',
				options.checkVisibility() ? '&#xf068;' : '&#xf067;');
		});

		// Aide contextuelle.
		App.click('#search_infos_link', () =>
		{
			const i = _q('#search_infos');
			i.style.display = i.checkVisibility() ? 'none' : 'block';
		});
	}

	/**
	 * Gestion du mode sélection.
	 *
	 * @return void
	 */
	function _selection()
	{
		let ajax;

		// Sélection d'un fichier sur la page des fichiers.
		if (_q('#item') && _q('#link_selection'))
		{
			Gallery.updateSelection = (action, r) =>
			{
				App.toggleClass('#item', 'selection', action == 'add');
				App.toggleClass('#link_selection', 'add', action != 'add');
				App.toggleClass('#link_selection', 'remove', action == 'add');
				App.attr('#link_selection a', 'title', App.attr('#link_selection a',
					'data-l10n-' + (action == 'add' ? 'remove' : 'add')));
			};
			App.click('#link_selection a', () =>
			{
				if (ajax)
				{
					return;
				}
				const action = App.hasClass('#item', 'selection') ? 'remove' : 'add';
				ajax = App.ajax(
				{
					section: 'selection',
					action: 'selection-' + action,
					id: [ITEM.id]
				},
				{
					always: r => setTimeout(() => ajax = null, 500),
					error: r => console.log('error: ' + r.message),
					success: r =>
					{
						Gallery.updateSelection(action, r);
						if (GALLERY.q_pageless.match(/\/selection(\/\d+)$/) && action == 'remove')
						{
							window.location = ITEM.link;
						}
						Gallery.reloadDiaporama();
					}
				});
			});
		}

		// Sélection de fichiers par les vignettes.
		if (typeof BOX_SELECTION != 'undefined')
		{
			// Mise à jour des informations de sélection.
			if (!_q('#item'))
			{
				Gallery.updateSelection = stats =>
				{
					// Nombre maximum de fichiers (pour les invités uniquement).
					const max_items = parseInt(BOX_SELECTION.params.max_items);

					// Nombre de fichiers dans la sélection.
					App.remove('#box_selection .selection_title > *');
					App.append('#box_selection .selection_title', stats.count > 0
						? '<span class="selection_count"><a></a><a></a></span>'
						: '<span class="selection_count"><span>0</span></span>');
					App.attr('#box_selection .selection_count a:first-child',
						'href', BOX_SELECTION.params.link);
					App.text('#box_selection .selection_count a:first-child', stats.count);
					if (BOX_SELECTION.params.is_admin)
					{
						App.attr('#box_selection .selection_count a + a',
							'href', BOX_SELECTION.params.link_admin);
						App.attr('#box_selection .selection_count a + a',
							'title', BOX_SELECTION.l10n.admin);
						App.html('#box_selection .selection_count a + a', '&#xe90c;');
					}
					else
					{
						App.remove('#box_selection .selection_count a + a');
					}
					App.append('#box_selection .selection_count',
						max_items > 0 ? `/<span>${max_items}</span>` : '');

					// Poids total de la séletion.
					App.text('#selection_filesize', BOX_SELECTION.l10n.selection_filesize
						.replace('%s', stats.filesize_formated));

					// État des boutons d'action des formulaires.
					App.each('#box_selection input.action', elem =>
					{
						App.toggleClass(elem, 'disabled', stats.count < 1);
						elem.disabled = stats.count < 1;
					});

					// Vignettes.
					App.toggleClass('.thumbs_items', 'selection_max',
						max_items > 0 && stats.count >= max_items);

					// Mise à jour des stats.
					BOX_SELECTION.params.stats = stats;
				};
			}

			// Fonction executée lors d'un click sur l'icône de sélection.
			function select(item_id)
			{
				if (ajax)
				{
					return;
				}

				const selector = `dl[data-id="${item_id}"]`;
				const selected = App.hasClass(selector, 'selected');

				App.each(selector, dl =>
				{
					// Limitation du nombre de fichiers que l'on peut ajouter dans la sélection.
					if (!selected && BOX_SELECTION.params.max_items > 0
					&& BOX_SELECTION.params.stats.count >= BOX_SELECTION.params.max_items)
					{
						return;
					}

					// État de sélection de la vignette.
					App.toggleClass(dl, 'selected');
					App.html([dl, '.selection_icon'], selected ? '&#xea53;' : '&#xea52;');
				});

				// Mise à jour par Ajax.
				ajax = App.ajax(
				{
					section: 'selection',
					action: 'selection-' + (selected ? 'remove' : 'add'),
					id: [item_id]
				},
				{
					always: r => ajax = null,
					error: r => alert('error: ' + r.message),
					success: r =>
					{
						Gallery.updateSelection(r.stats);
						if (typeof DIAPORAMA != 'undefined' && DIAPORAMA.item_id == item_id)
						{
							Gallery.diaporama = null;
						}
					}
				});
			}

			// Ajout d'une icône sur les vignettes pour indiquer
			// si le fichier se trouve dans la sélection.
			App.each('.thumbs_items dl dt', elem =>
			{
				const icon = App.hasClass(elem.closest('dl'), 'selected')
					? '&#xea52;'
					: '&#xea53;';
				App.append(elem, `<span class="selection_icon">${icon}</span>`);
			});

			// Sélection d'un fichier sur écran de petite taille.
			App.on('.thumbs_items dt a', 'click', function(evt)
			{
				if (BOX_SELECTION.params.start && Gallery.isSmallScreen())
				{
					evt.preventDefault();
					select(App.attr(this.closest('dl'), 'data-id'));
				}
			});

			// Sélection d'un fichier sur grand écran à partir de l'icône de sélection.
			App.click('.thumbs_items dt .selection_icon', function()
			{
				select(App.attr(this.closest('dl'), 'data-id'));
			});
		}
	}

	/**
	 * Gestion des tableaux.
	 *
	 * @return void
	 */
	function _table()
	{
		// Tri des tableaux.
		// https://stackoverflow.com/questions/14267781#49041392
		const cellval = (tr, i) => tr.children[i].innerText || tr.children[i].textContent;
		const compare = (i, asc) => (a, b) => ((v1, v2) => v1 !== '' && v2 !== ''
			&& !isNaN(v1) && !isNaN(v2) ? v1 - v2 : v1.toString().localeCompare(v2))
			(cellval(asc ? a : b, i), cellval(asc ? b : a, i));

		App.click('table.sorter th', function()
		{
			const table = this.closest('table');
			Array.from(_qAll(table, 'tr:nth-child(n+2)'))
				.sort(compare(
					Array.from(this.parentNode.children)
						.indexOf(this), this.asc = !this.asc
				))
				.forEach(tr => table.appendChild(tr));
		});
	}

	/**
	 * Ajout de fichiers.
	 *
	 * @return void
	 */
	function _upload()
	{
		if (!_q('#upload'))
		{
			return;
		}

		// Gestion des listes d'albums.
		function select_cat_change()
		{
			const id = parseInt(this.selectedOptions[0].value);
			const key = UPLOAD.cat_list.k.indexOf(id);

			// Supprime toutes les listes enfants à partir de la liste courante.
			const i = parseInt(this.id.replace(/list_categories_/, ''));
			let n = i + 1;
			while (_q('#list_categories_' + n))
			{
				App.remove(`#list_categories_${n},#upload_sep_${n}`);
				n++;
			}

			// Crée la nouvelle liste enfant si c'est une catégorie.
			if (UPLOAD.cat_list.k.includes(id) && UPLOAD.cat_list.v[key].type == 1)
			{
				select_cat_create(id, i + 1);
			}

			// Album sélectionné.
			if (key > 0 && UPLOAD.cat_list.v[key].type == 0)
			{
				// On indique le chemin complet de l'album sélectionné.
				function insert_name(key, position)
				{
					const title = UPLOAD.cat_list.v[key].title;
					const link = _q(`#browse li[id*="{${UPLOAD.cat_list.v[key].id}}"] a`);
					if (link)
					{
						const a = document.createElement('a');
						App.attr(a, 'href', link.href);
						App.text(a, title);
						App.prepend('#select_path', a);
						return;
					}
					App.prependText('#select_path', title);
				}

				App.empty('#select_path');
				insert_name(key);

				let parent_id = UPLOAD.cat_list.v[key].parent;
				let parent_key = UPLOAD.cat_list.k.indexOf(parent_id);
				while (UPLOAD.cat_list.k.includes(parent_id) && parent_id > 1)
				{
					App.prepend('#select_path', '<span> / </span>');
					insert_name(parent_key);

					parent_id = UPLOAD.cat_list.v[parent_key].parent;
					parent_key = UPLOAD.cat_list.k.indexOf(parent_id);
				}

				App.attr('#select_path', 'class', 'message_success');

				// On indique que l'envoi de fichier peut commencer.
				upload.options.ajaxData.album_id = id;
				upload.options.albumSelected = true;
				if (!_q('#upload_clear').disabled)
				{
					_q('#upload_start').disabled = false;
				}

				// Attribut "action" du formulaire.
				const action = _q('#upload_form').action;
				App.attr('#upload_form', 'action', action.match(/user-upload$/)
					? action + '/album/' + id
					: action.replace(/(user-upload\/album\/)\d+$/, '$1' + id));
			}

			// Aucun album sélectionné.
			else
			{
				App.attr('#select_path', 'class', 'message_info');
				App.text('#select_path', UPLOAD.cat_text.none);
				upload.options.ajaxData.album_id = 0;
				upload.options.albumSelected = false;
				_q('#upload_start').disabled = true;
			}

			// On passe l'identifiant de l'album sélectionné
			// dans un champ du formulaire.
			App.val('#upload_form input[name="cat_id"]', upload.options.ajaxData.album_id);
		}
		function select_cat_create(cat_id, i)
		{
			// Séparateur.
			if (i > 1)
			{
				App.append('#list_categories', `<span id="upload_sep_${i}"> / </span>`);
			}

			// Liste des catégories.
			const select = App.createElement(`<select id="list_categories_${i}"></select>`);
			App.on(select, 'change', select_cat_change);

			// Le premier élément de la liste sert à
			// indiquer de sélectionner un album.
			const option_first = document.createElement('option');
			App.text(option_first, `[${UPLOAD.cat_text.select}]`);
			App.prepend(select, option_first);

			// Catégories de la liste.
			for (const key in UPLOAD.cat_list.k)
			{
				if (UPLOAD.cat_list.v[key].parent == cat_id)
				{
					const option = document.createElement('option');
					App.val(option, UPLOAD.cat_list.k[key]);
					App.text(option, UPLOAD.cat_list.v[key].title);
					App.append(select, option);
				}
			}
			App.append('#list_categories', select);
		}

		// Initialisation.
		const upload = new Upload('upload_', UPLOAD.options);
		select_cat_create(1, 1);
		_q('#list_categories_1').focus();

		// Album auto-sélectionné.
		function autoselect()
		{
			const autoselect = parseInt(App.val('#upload_form input[name="cat_id"]'));
			if (autoselect > 1 && UPLOAD.cat_list.k.includes(autoselect))
			{
				const parents = [];
				const select = (n, id) =>
					_q(`#list_categories_${n - 1} option[value="${id}"]`).selected = true;
				let parent_id = UPLOAD.cat_list.v[UPLOAD.cat_list.k.indexOf(autoselect)].parent;
				while (UPLOAD.cat_list.k.includes(parent_id) && parent_id > 1)
				{
					parents.push(parent_id);
					parent_id = UPLOAD.cat_list.v[UPLOAD.cat_list.k.indexOf(parent_id)].parent;
				}
				let n = 2;
				for (let i = parents.length - 1; i > -1; i--)
				{
					if (!_q('#list_categories_' + n))
					{
						select_cat_create(parents[i], n);
					}
					select(n, parents[i]);
					n++;
				}
				select(n, autoselect);
				App.trigger('#list_categories_' + (n - 1), 'change');
			}
		}
		autoselect();

		// On génère la liste des albums pour pouvoir récupérer les liens
		// de l'album sélectionné et de ses catégories parentes.
		if (App.hasClass('#menu_gallery', 'browse'))
		{
			Gallery.boxesArrow.browse(autoselect);
		}
	}

	/**
	 * Gestion des vidéos.
	 *
	 * @return void
	 */
	function _video()
	{
		// Réglage du volume.
		App.each('video', elem => elem.volume = 0.5);

		// On désactive le menu contextuel des vidéos
		// s'il y a interdiction de les télécharger.
		App.on('#item video', 'contextmenu', function(evt)
		{
			if (App.attr(this, 'controlsList') == 'nodownload')
			{
				evt.preventDefault();
			}
		});

		// Captures vidéos.
		if (typeof CAPTURES !== 'undefined')
		{
			VideoCaptures(CAPTURES);
		}
	}

	/**
	 * Visionneuse d'image.
	 *
	 * @return void
	 */
	function _viewer()
	{
		App.click('#item_container.viewer', function()
		{
			function close()
			{
				App.remove('#viewer');
				App.off(window, {'resize': resize, 'keyup': keyup});
				content_location();
			}
			function keyup(evt)
			{
				if (evt.keyCode == 27)
				{
					close();
				}
			}
			function resize()
			{
				const margin = parseInt(getComputedStyle(_q('#viewer'))['paddingLeft']) * 2;
				_q('#viewer').style.width =
					(document.documentElement.clientWidth - margin) + 'px';
				_q('#viewer').style.height =
					(document.documentElement.clientHeight - margin) + 'px';
			}
			function content_location()
			{
				window.location = document.location.href
					.replace(document.location.hash, '') + '#content';
				_q('#item_link').focus();
			}

			let source = _q(this, 'a').href || _q(this, 'img').src;

			if (!source || _q('#viewer'))
			{
				return;
			}

			// Si l'image a été agrandie, on la redimensionne dans sa taille d'origine.
			if (App.hasClass(this, 'large'))
			{
				App.removeClass(this, 'large');
				content_location();
				return;
			}

			// Si la différence de hauteur est suffisante, on agrandi
			// simplement l'image dans le conteneur. Cela permet d'avoir
			// une plus grande taille pour les images au format portrait.
			const current_image_height = _q(this, 'img').offsetHeight;
			App.addClass(this, 'large');
			if (_q(this, 'img').offsetHeight > (current_image_height + 85))
			{
				return;
			}
			App.removeClass(this, 'large');

			// Création de la visionneuse.
			const viewer = App.createElement('<div id="viewer"><img></div>');
			App.attr([viewer, 'img'],
			{
				'data-type': App.attr([this, 'img'], 'data-type'),
				'src': source + (/\?/.test(source) ? '&' : '?') + Math.random()
			});
			App.click(viewer, close);
			App.prepend('main', viewer);
			resize();

			// Gestion des événements.
			App.on(window, {'resize': resize, 'keyup': keyup});

			if (!_q(this, 'a').href)
			{
				App.on('#viewer img', 'contextmenu', evt => evt.preventDefault());
			}
		});
	}

	/**
	 * Géolocalisation.
	 *
	 * @return void
	 */
	function _worldmap()
	{
		if (!_q('#worldmap'))
		{
			return;
		}

		const layer = App.attr('#worldmap', 'data-layer');
		const lat = App.attr('#worldmap', 'data-lat');
		const lng = App.attr('#worldmap', 'data-long');

		// Initialisation de la carte.
		const map = L.map('worldmap',
		{
			center: [lat, lng],
			zoom: App.attr('#worldmap', 'data-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>'
		});
		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>'
		});
		if (layer == 'satellite')
		{
			esri.addTo(map);
		}
		else
		{
			osm.addTo(map);
		}
		const layers = {};
		layers[App.attr('#worldmap', 'data-l10n-map')] = osm;
		layers[App.attr('#worldmap', 'data-l10n-satellite')] = esri;
		L.control.layers(layers, {}, {position: 'bottomleft'}).addTo(map);

		// Marqueurs.
		const marker_cat = L.icon(
		{
			iconUrl: GALLERY.path + '/images/markers/marker-category.png',
			iconSize: [20, 34],
			iconAnchor: [10, 34],
			popupAnchor: [0, -39],
			shadowUrl: GALLERY.path + '/images/markers/marker-shadow.png',
			shadowSize: [54, 34],
			shadowAnchor: [27, 34]
		});
		const marker_item = 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]
		});

		// Page des fichiers.
		if (_q('#item_geolocation'))
		{
			const marker = L.marker([lat, lng], {icon: marker_item}).addTo(map);
		}

		// Page "carte du monde".
		else
		{
			// Conteneurs à vignettes.
			const popup_cat =
				'<div id="popup_thumbs">' +
					'<div id="thumbs_cat"></div>' +
				'</div>';
			const popup_items =
				'<div id="popup_thumbs">' +
					'<div class="thumbs_items standard"></div>' +
				'</div>';

			// Types de vignettes.
			const tb_image =
				'<dl>' +
					'<dt><a><img></a></dt>' +
					'<dd></dd>' +
				'</dl>';
			const tb_video =
				'<dl class="video">' +
					'<dt><a><span class="duration"></span><img></a></dt>' +
					'<dd></dd>' +
				'</dl>';

			// Création de la pagination entre les vignettes.
			function pagination(thumbs, items_count)
			{
				if (items_count > 1)
				{
					const tb_pagination = App.createElement(
						'<div id="thumbs_pagination">' +
							'<a href="javascript:;" id="thumbs_pagination_prev">&#xf053;</a>' +
							`<span>1/${items_count}</span>` +
							'<a href="javascript:;" id="thumbs_pagination_next">&#xf054;</a>' +
						'</div>');
					App.click([tb_pagination, 'a'], function()
					{
						const button = this.id.replace('thumbs_pagination_', '');
						const text = App.text('#thumbs_pagination span').split('/');
						const current = parseInt(text[0]);
						const count = parseInt(text[1]);
						const item = current + (button == 'prev' ? -1 : 1);

						_q('#popup_' + current).style.display = 'none';
						_q('#popup_' + item).style.display = 'block';

						_q('#thumbs_pagination_' + (button == 'prev' ? 'next' : 'prev'))
							.style.visibility = 'visible';
						if ((button == 'prev' && item == 1)
						 || (button == 'next' && item == count))
						{
							_q('#thumbs_pagination_' + button).style.visibility = 'hidden';
						}

						App.text('#thumbs_pagination span', item + '/' + count);
					});
					App.append(thumbs, tb_pagination);
				}
			}

			for (const i in MARKERS)
			{
				// Fichiers.
				const items_count = Object.keys(MARKERS[i].items).length;
				if (items_count)
				{
					const marker = L.marker(
						[MARKERS[i].lat, MARKERS[i].lng], {icon: marker_item}
					).addTo(map);
					const thumbs = App.createElement(popup_items);

					let num = 1;
					for (const n in MARKERS[i].items)
					{
						const item = MARKERS[i].items[n];
						const dl = App.createElement(item.type == 'video' ? tb_video : tb_image);

						App.attr(dl, 'id', 'popup_' + num++);
						App.attr([dl, 'a'], 'href', item.link + '#content');
						App.text([dl, 'dd'], item.title);
						App.attr([dl, 'img'], 'src', item.source);

						if (item.type == 'video')
						{
							App.addClass(dl, 'video');
							App.attr([dl, 'span'], 'id', 'videoduration_' + item.id);
							App.text([dl, 'span'], item.duration);
							App.attr([dl, 'img'], 'data-video-id', item.id);
						}

						App.append([thumbs, '.thumbs_items'], dl);
					}

					pagination(thumbs, items_count);
					marker.bindPopup(thumbs);
				}

				// Catégories.
				const cat_count = Object.keys(MARKERS[i].categories).length;
				if (cat_count)
				{
					const marker = L.marker(
						[MARKERS[i].lat, MARKERS[i].lng], {icon: marker_cat}
					).addTo(map);
					const thumbs = App.createElement(popup_cat);

					let num = 1;
					for (const n in MARKERS[i].categories)
					{
						const cat = MARKERS[i].categories[n];
						const dl = App.createElement(tb_image);

						App.attr(dl, 'id', 'popup_' + num++);
						App.attr([dl, 'a'], 'href', cat.link + '#content');
						App.text([dl, 'dd'], cat.title);
						App.attr([dl, 'img'], 'src', cat.source);

						App.append([thumbs, '#thumbs_cat'], dl);
					}

					pagination(thumbs, cat_count);
					marker.bindPopup(thumbs);
				}
			}
		}
	}
}();