// ==UserScript==
// @name JVC_ImageViewer
// @namespace http://tampermonkey.net/
// @version 2.0.6
// @description Naviguer entre les images d'un post sous forme de slideshow en cliquant sur une image sans ouvrir NoelShack.
// @author HulkDu92
// @match https://*.jeuxvideo.com/forums/*
// @match https://*.jeuxvideo.com/profil/*
// @match https://*.jeuxvideo.com/messages-prives/*
// @match https://jvarchive.com/*
// @match https://jvarchive.st/*
// @require https://cdn.jsdelivr.net/npm/[email protected]/dist/panzoom.min.js
// @grant GM_download
// @grant GM.xmlHttpRequest
// @connect image.noelshack.com
// @run-at document-end
// @license MIT
// @icon https://image.noelshack.com/fichiers/2024/41/3/1728506420-image-viewer-icon.png
// ==/UserScript==
(function() {
'use strict';
class Panzoom {
constructor(imgElement, callbacks = {}) {
this.imgElement = imgElement;
this.imageElementScale = 1;
this.isDragging = false;
this.isSwiping = false;
this.busy = false;
this.timeoutIdBusy = null;
this.timeoutIdZooming = null;
this.timeoutIdPanning = null;
// Attribuer des callbacks par défaut
// TODO remplacer ça par l'emission de signaux "swipe next", "swipe right" , "swipe close"
this.showPreviousImage = callbacks.showPreviousImage || (() => {});
this.showNextImage = callbacks.showNextImage || (() => {});
this.closeViewer = callbacks.closeViewer || (() => {});
if (imgElement.complete) {
this.initializePanzoom();
} else {
imgElement.onload = this.initializePanzoom;
}
}
initializePanzoom() {
this.panzoomInstance = panzoom(this.imgElement, {
contain: 'inside',
bounds: true,
boundsPadding: 1,
zoomDoubleClickSpeed: 1,
panOnlyWhenZoomed: true,
smoothScroll: true,
startScale: 1,
});
this.panzoomInstance.setMinZoom(1);
// Ecouter l'événement 'zoom' pour mettre à jour l'échelle
this.panzoomInstance.on('zoom', (e) => {
const transform = e.getTransform();
this.imageElementScale = transform.scale;
this.imgElement.parentElement.style.zIndex = this.imageElementScale > 1 ? 10002 : '';
this.isZooming = true;
clearTimeout(this.timeoutIdZooming); // Réinitialise le timer si un nouveau zoom survient
this.timeoutIdZooming = setTimeout(() => {
this.isZooming = false;
}, 250);
});
this.panzoomInstance.on('transform', () => {
// console.log("busy");
this.markBusy.bind(this);
});
// Ecouteur pour le pan
this.panzoomInstance.on('panstart', () => {
this.isDragging = true;
});
this.panzoomInstance.on('panend', () => {
// Attendre un court délai avant de remettre isDragging à false
clearTimeout(this.timeoutIdPanning); // Réinitialise le timer si un nouveau zoom survient
this.timeoutIdPanning = setTimeout(() => {
this.isDragging = false;
}, 250);
});
// Ajout des écouteurs d'événements tactiles pour le swipe
this.imgElement.addEventListener('touchstart', (event) => this.handleTouchEvent(event));
this.imgElement.addEventListener('touchmove', (event) => this.handleTouchEvent(event));
this.imgElement.addEventListener('touchend', (event) => this.handleTouchEvent(event));
}
reset() {
this.resetZoom();
this.resetDrag();
}
/* destroy() {
this.panzoomInstance.off('transform', this.markBusy.bind(this));
this.panzoomInstance.destroy();
}
*/
markBusy() {
this.busy = true;
clearTimeout(this.timeoutIdBusy); // Réinitialise le timer si une nouvelle transformation survient
this.timeoutIdBusy = setTimeout(() => {
this.busy = false;
}, 250);
}
isBusy() {
// console.log("isBusy: ", this.busy || this.isSwiping || this.isDragging);
return this.busy || this.isSwiping || this.isDragging || this.isZooming;
}
resetZoom() {
this.panzoomInstance.zoomAbs(0, 0, 1);
this.imgElement.style.transform = 'scale(1)';
this.imgElement.style.transformOrigin = 'center center';
this.imgElement.parentElement.style.zIndex = '';
}
// Réinitialiser la position du drag de l'image
resetDrag() {
this.imgElement.style.left = '0px';
this.imgElement.style.top = '0px';
}
handleTouchEvent(event) {
switch (event.type) {
case 'touchstart':
if (event.touches.length === 1) {
if (this.imageElementScale > 1) {
// Ne rien faire si l'image est zoomée
} else {
// Démarrer le swipe
this.handleSwipeStart(event);
}
}
break;
case 'touchmove':
if (event.touches.length === 1) {
if (this.imageElementScale > 1) {
// Ne rien faire si l'image est zoomée
} else {
this.handleSwipeMove(event);
}
}
break;
case 'touchend':
if (event.touches.length === 1) {
if (this.imageElementScale > 1) {
// Ne rien faire si l'image est zoomée
}
} else if (event.touches.length === 0) {
if (this.isSwiping) {
this.handleSwipeEnd(event);
}
}
break;
}
}
handleSwipeStart(event) {
if (event.touches.length === 1) {
if (this.imageElementScale > 1 || this.isZooming) {
return; // Ne pas commencer le swipe si l'image est zoomée
}
//this.isSwiping = true;
this.startX = event.touches[0].clientX;
this.startY = event.touches[0].clientY;
//this.imgElement.style.transition = 'none';
}
}
handleSwipeMove(event) {
if (event.touches.length === 1) {
if (this.imageElementScale > 1 || this.isZooming) {
return; // Ne pas swipe si l'image est zoomée
}
this.isSwiping = true;
this.currentX = event.touches[0].clientX;
this.currentY = event.touches[0].clientY;
const deltaX = this.currentX - this.startX;
const deltaY = this.currentY - this.startY;
if (this.imageElementScale === 1) {
// Appliquer le déplacement en fonction de la direction du swipe
if (Math.abs(deltaY) > Math.abs(deltaX)) {
// Swipe vertical
this.imgElement.style.transform = `translateY(${deltaY}px)`;
this.imgElement.style.opacity = Math.max(1 - Math.abs(deltaY) / 300, 0);
} else {
// Swipe horizontal
//this.imgElement.style.transform = `translateX(${deltaX}px)`;
}
}
}
}
handleSwipeEnd(event) {
if (event.touches.length === 0) {
this.initialDistance = null;
}
if (this.imageElementScale > 1 || this.isZooming) {
return; // Ne pas swipe si l'image est zoomée
}
if (this.isSwiping) {
const deltaX = this.currentX - this.startX;
const deltaY = this.currentY - this.startY;
// Si le mouvement est suffisamment grand, on change d'image
if (Math.abs(deltaX) > 50) {
if (deltaX > 0) {
this.showPreviousImage();
} else {
this.showNextImage();
}
}
if (this.imageElementScale === 1) {
// Si le mouvement est suffisamment grand verticalement, on ferme le visualiseur
if (Math.abs(deltaY) > 50) {
this.closeViewer();
} else {
this.imgElement.style.opacity = 1;
this.imgElement.style.transform = '';
}
}
setTimeout(() => {
this.isSwiping = false;
}, 50);
}
// Réinitialiser le zIndex
this.imgElement.style.zIndex = '';
}
destroy() {
if (this.panzoomInstance) { // Check if panzoomInstance exists
this.panzoomInstance.off('transform', this.markBusy.bind(this));
this.panzoomInstance.dispose(); // Call the original panzoom library's destroy method
this.panzoomInstance = null; // Important to clear the reference
}
this.panzoomInstance.off('transform', this.markBusy.bind(this));
this.panzoomInstance.destroy();
}
}
class ImageViewer {
constructor() {
if (ImageViewer.instance) {
return ImageViewer.instance;
}
this.images = [];
this.currentIndex = 0;
this.overlay = null;
this.imgElement = null;
this.spinner = null;
this.prevButton = null;
this.nextButton = null;
this.closeButton = null;
this.infoText = null;
this.downloadButton = null;
this.searchButton = null;
this.optionButton = null;
this.playPauseButton = null;
this.fullScreenButton = null;
this.freezeButton = null;
this.indicatorsContainer = null;
this.indicators = [];
this.isViewerOpen = false;
this.thumbnailPanel = null;
this.previousThumbnail = null;
this.defaultImageWidth = Math.min(window.innerWidth, 1200);
this.panzoom = null;
ImageViewer.instance = this;
this.handlePopState = this.handlePopState.bind(this);
this.createOverlay();
this.updateImage();
}
// Crée et configure les éléments du visualiseur d'images (overlay, boutons, texte d'information, etc.)
createOverlay() {
this.overlay = this.createElement('div', {
position: 'fixed',
top: 0,
left: 0,
width: '100%',
height: '100%',
backgroundColor: 'rgba(0, 0, 0, 0.8)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 10000, //getElementZIndex('.header.js-header.header--affix', 10000),
});
this.imgElement = this.createElement('img', {
transition: 'opacity 0.3s',
opacity: 0,
cursor: 'pointer',
objectFit: 'contain',
maxWidth: '100%',
maxHeight: '100%',
width: '100%', /* image box size as % of container, see step 1 */
height: '100%',
});
this.imgContainer = this.createElement('div', {
maxWidth: '90%',
maxHeight: '80%',
// width: 'auto', // Important: Allow width to adjust to content
// height: 'auto', // Important: Allow height to adjust to content
display: 'flex', // Ensure flex layout to center the image
justifyContent: 'center', // Horizontally center
alignItems: 'center', // Vertically center
});
this.imgContainer.appendChild(this.imgElement);
this.spinner = this.createSpinner();
this.prevButton = this.createButton('<', 'left');
this.nextButton = this.createButton('>', 'right');
this.closeButton = this.createCloseButton();
this.infoText = this.createInfoText();
this.downloadButton = this.createDownloadButton();
this.searchButton = this.createSearchButton();
this.optionButton = this.createOptionButton();
this.thumbnailPanel = this.createThumbnailPannel();
this.indicatorsContainer = this.createElement('div', {
display: 'flex',
justifyContent: 'center',
marginBottom: '10px 0',
position: 'absolute',
bottom: '40px',
});
// Ajouter ici la logique de placement en bas à droite
const buttonContainer = this.createElement('div', {
position: 'absolute',
bottom: '30px',
right: '10px',
display: 'flex',
flexDirection: 'column',
gap: '5px',
zIndex: 10001,
});
// Ajouter les boutons dans ce conteneur
buttonContainer.append(this.searchButton, this.downloadButton, this.optionButton);
this.playPauseButton = this.createPlayPauseButton();
this.fullScreenButton = this.createFullScreenButton();
this.freezeButton = document.createElement('div'); // Pour le moment pas de bouton utilisable juste un element HTML vide
// Conteneur pour les boutons de manipulation d'image
const imageControlsContainer = this.createElement('div', {
position: 'absolute',
top: '80px', // Ajuster la position verticale si nécessaire
right: '20px',
display: 'flex',
alignItems: 'center',
gap: '5px',
zIndex: 10002,
});
imageControlsContainer.append(this.playPauseButton, this.fullScreenButton, this.closeButton);
this.optionsMenu = this.createOptionsMenu(); // must be last
this.overlay.append(
this.imgContainer,
this.spinner,
this.infoText,
this.prevButton,
this.nextButton,
imageControlsContainer,
// this.closeButton,
buttonContainer, // Ajouter le conteneur de boutons à l'overlay
this.indicatorsContainer
);
// Positionner le menu d'options à gauche de buttonContainer
this.overlay.append(this.optionsMenu);
// Événements associés aux boutons et à l'overlay
this.resetHideButtons();
this.addEventListeners();
this.addInteractionListeners();
document.body.appendChild(this.overlay);
}
// Crée un élément HTML avec des styles
createElement(tag, styles = {}) {
const element = document.createElement(tag);
Object.assign(element.style, styles);
return element;
}
// Crée le bouton précédent ou suivant
createButton(text, position) {
const isMobileDevice = isMobile();
const button = this.createElement('button', {
position: 'absolute',
[position]: '5px',
backgroundColor: 'rgba(0, 0, 0, 0.6)',
color: 'white',
fontSize: isMobileDevice ? '18px' : '22px',//'22px',
border: 'none',
borderRadius: '50%',
width: isMobileDevice ? '35px' : '40px',//'40px',
height: isMobileDevice ? '35px' : '40px',//'40px',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0px 4px 8px rgba(0, 0, 0, 0.6)',
transition: 'background-color 0.3s, transform 0.3s'
});
//button.textContent = text;*
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('width', '24');
svg.setAttribute('height', '24');
svg.setAttribute('fill', 'white');
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', position === 'left'
? 'M15.41 16.59L10.83 12l4.58-4.59L14 6l-6 6 6 6z' // Icône flèche gauche
: 'M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6z'); // Icône flèche droite
svg.appendChild(path);
button.appendChild(svg);
this.addButtonEffects(button);
return button;
}
createDownloadButton() {
const isMobileDevice = isMobile();
const button = this.createElement('button', {
position: 'relative',
backgroundColor: 'rgba(0, 0, 0, 0.5)',
color: 'white',
fontSize: isMobileDevice ? '12px' : '10px',
border: '1px solid rgba(255, 255, 255, 0.3)',
borderRadius: '50%',
padding: '0',
cursor: 'pointer',
zIndex: 10001,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: isMobileDevice ? '37px' : '45px',
height: isMobileDevice ? '37px' : '45px',
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.5)',
transition: 'transform 0.3s ease, background-color 0.3s ease',
});
button.setAttribute('title', 'Enregistrer l\'image');
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('width', isMobileDevice ? '18' : '22');
svg.setAttribute('height', isMobileDevice ? '18' : '22');
svg.setAttribute('fill', 'none');
svg.setAttribute('stroke', 'currentColor');
svg.setAttribute('stroke-linecap', 'round');
svg.setAttribute('stroke-linejoin', 'round');
svg.setAttribute('stroke-width', '2');
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', 'M6 21h12M12 3v14m0 0l5-5m-5 5l-5-5');
svg.appendChild(path);
button.appendChild(svg);
this.addButtonEffects(button);
return button;
}
createSearchButton() {
const isMobileDevice = isMobile();
const button = this.createElement('button', {
position: 'relative',
backgroundColor: 'rgba(0, 0, 0, 0.6)',
color: 'white',
fontSize: isMobileDevice ? '12px' : '10px',
border: '1px solid rgba(255, 255, 255, 0.3)',
borderRadius: '50%',
padding: '0',
cursor: 'pointer',
zIndex: 10001,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: isMobileDevice ? '37px' : '45px',
height: isMobileDevice ? '37px' : '45px',
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.5)',
transition: 'transform 0.3s ease, background-color 0.3s ease',
});
button.setAttribute('title', 'Rechercher par image');
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
svg.setAttribute('width', isMobileDevice ? '18' : '22');
svg.setAttribute('height', isMobileDevice ? '18' : '22');
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('fill', 'currentColor');
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('fill', 'currentColor');
path.setAttribute('d', 'M18 13v7H4V6h5.02c.05-.71.22-1.38.48-2H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2v-5l-2-2zm-1.5 5h-11l2.75-3.53l1.96 2.36l2.75-3.54zm2.8-9.11c.44-.7.7-1.51.7-2.39C20 4.01 17.99 2 15.5 2S11 4.01 11 6.5s2.01 4.5 4.49 4.5c.88 0 1.7-.26 2.39-.7L21 13.42L22.42 12L19.3 8.89zM15.5 9a2.5 2.5 0 0 1 0-5a2.5 2.5 0 0 1 0 5z');
svg.appendChild(path);
button.appendChild(svg);
button.addEventListener('click', () => this.searchImageOnGoogle());
this.addButtonEffects(button);
return button;
}
createOptionButton() {
const isMobileDevice = isMobile();
const button = this.createElement('button', {
position: 'relative',
backgroundColor: 'rgba(0, 0, 0, 0.6)',
color: 'white',
fontSize: isMobileDevice ? '12px' : '10px',
border: '1px solid rgba(255, 255, 255, 0.3)',
borderRadius: '50%',
padding: '0',
cursor: 'pointer',
zIndex: 10001,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: isMobileDevice ? '37px' : '45px',
height: isMobileDevice ? '37px' : '45px',
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.5)',
transition: 'transform 0.3s ease, background-color 0.3s ease',
});
button.setAttribute('title', 'Personnaliser');
// Création du SVG avec trois points alignés verticalement
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('width', isMobileDevice ? '18' : '22');
svg.setAttribute('height', isMobileDevice ? '18' : '22');
svg.setAttribute('fill', 'currentColor');
// Création des trois cercles pour les trois points
const circle1 = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circle1.setAttribute('cx', '12');
circle1.setAttribute('cy', '5');
circle1.setAttribute('r', '2');
const circle2 = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circle2.setAttribute('cx', '12');
circle2.setAttribute('cy', '12');
circle2.setAttribute('r', '2');
const circle3 = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circle3.setAttribute('cx', '12');
circle3.setAttribute('cy', '19');
circle3.setAttribute('r', '2');
// Ajout des cercles dans le SVG
svg.appendChild(circle1);
svg.appendChild(circle2);
svg.appendChild(circle3);
// Ajout du SVG dans le bouton
button.appendChild(svg);
// Créer le menu d'options
//this.optionsMenu = this.createOptionsMenu();
this.addButtonEffects(button);
return button;
}
// Crée le bouton de fermeture
createCloseButton() {
const isMobileDevice = isMobile();
const button = this.createElement('button', {
position: 'relative',
backgroundColor: 'rgba(0, 0, 0, 0.8)',
color: 'white',
fontSize: isMobileDevice ? '18px' : '16px',
//border: 'none',
border: '1px solid rgba(255, 255, 255, 0.3)',
borderRadius: '50%',
width: isMobileDevice ? '40px' : '35px',
height: isMobileDevice ? '40px' : '35px',
cursor: 'pointer',
zIndex: 99999999
});
button.textContent = '✕';
this.addButtonEffects(button);
return button;
}
// Crée la zone d'affichage du texte d'information (numéro d'image)
createInfoText() {
return this.createElement('div', {
position: 'absolute',
//top: '80px',
bottom: '0px',
//left: '15px',
right: '10px',
color: 'white',
fontSize: '12px',
backgroundColor: 'rgba(5, 5, 5, 0.5)',
padding: '5px',
borderRadius: '5px',
zIndex: 10001
});
}
// Crée un spinner pour indiquer le chargement de l'image
createSpinner() {
const spinner = this.createElement('div', {
position: 'absolute',
border: '8px solid #f3f3f3',
borderTop: '8px solid #3498db',
borderRadius: '50%',
width: '50px',
height: '50px',
animation: 'spin 1s linear infinite',
zIndex: 10001
});
return spinner;
}
createOptionsMenu() {
const optionsMenu = this.createElement('div', {
position: 'relative',
backgroundColor: 'rgba(5, 5, 5, 0.8)',
color: 'white',
padding: '10px',
borderRadius: '5px',
zIndex: 10001,
display: 'none', // Caché par défaut
flexDirection: 'column',
});
optionsMenu.style.position = 'absolute';
optionsMenu.style.bottom = '20px';
optionsMenu.style.right = '60px';
optionsMenu.appendChild(this.createCheckboxOption(
'Toujours afficher les boutons de navigation',
false,
this.freezeButton,
(checked) => {
checked ? this.freezeButton.removeAttribute('data-hidden-by-options') : this.freezeButton.setAttribute('data-hidden-by-options', 'true');
}
));
optionsMenu.appendChild(this.createCheckboxOption(
'Afficher le bouton de téléchargement',
true,
this.downloadButton,
(checked) => {
this.downloadButton.style.display = checked ? 'block' : 'none';
checked ? this.downloadButton.removeAttribute('data-hidden-by-options') : this.downloadButton.setAttribute('data-hidden-by-options', 'true');
}
));
optionsMenu.appendChild(this.createCheckboxOption(
'Afficher les miniatures',
true,
this.thumbnailPanel,
(checked) => {
this.thumbnailPanel.style.display = checked ? 'block' : 'none';
checked ? this.thumbnailPanel.removeAttribute('data-hidden-by-options') : this.thumbnailPanel.setAttribute('data-hidden-by-options', 'true');
}
));
optionsMenu.appendChild(this.createCheckboxOption(
'Afficher le bouton Google Lens',
false,
this.searchButton,
(checked) => {
this.searchButton.style.display = checked ? 'block' : 'none';
checked ? this.searchButton.removeAttribute('data-hidden-by-options') : this.searchButton.setAttribute('data-hidden-by-options', 'true');
}
));
optionsMenu.appendChild(this.createCheckboxOption(
'Afficher le bouton Full Screen',
false,
this.fullScreenButton,
(checked) => {
this.fullScreenButton.style.display = checked ? 'block' : 'none';
checked ? this.fullScreenButton.removeAttribute('data-hidden-by-options') : this.fullScreenButton.setAttribute('data-hidden-by-options', 'true');
}
));
optionsMenu.appendChild(this.createCheckboxOption(
'Afficher le bouton Slide Auto',
false,
this.playPauseButton,
(checked) => {
this.playPauseButton.style.display = checked ? 'block' : 'none';
checked ? this.playPauseButton.removeAttribute('data-hidden-by-options') : this.playPauseButton.setAttribute('data-hidden-by-options', 'true');
}
));
return optionsMenu;
}
// Fonction pour créer une option avec une case à cocher
_createCheckboxOption(labelText, isChecked = false, elementDepend, onChange) {
const container = this.createElement('div', {
display: 'flex',
alignItems: 'center',
margin: '5px 0',
cursor: 'pointer',
userSelect: 'none',
});
const checkboxId = `jvcimageviwer-checkbox-${labelText.replace(/\s+/g, '-').toLowerCase()}`;
// Mettre un drapeau indiquant si l'element doit etre caché ou pas si il y a eu une réponse dans le localStorage
const storedValue = localStorage.getItem(checkboxId);
if (storedValue !== null) {
isChecked = (storedValue === "true");
}
if (elementDepend) {
elementDepend.style.display = isChecked ? 'block' : 'none';
isChecked ? elementDepend.removeAttribute('data-hidden-by-options')
: elementDepend.setAttribute('data-hidden-by-options', 'true');
}
const checkbox = this.createElement('input');
checkbox.setAttribute('type', 'checkbox');
checkbox.checked = isChecked;
// Donne un ID unique à la case à cocher pour l'associer au label
checkbox.setAttribute('id', checkboxId);
// Écouteur d'événement pour changer la valeur
checkbox.addEventListener('change', (e) => {
onChange(e.target.checked);
localStorage.setItem(checkboxId, e.target.checked);
});
const label = this.createElement('label');
label.textContent = labelText;
label.setAttribute('for', checkboxId);
label.style.marginLeft = '10px';
container.append(checkbox, label);
// Ajout d'un écouteur d'événement sur le conteneur pour activer la case à cocher
container.addEventListener('click', () => {
if (event.target !== checkbox && event.target !== label) {
checkbox.checked = !checkbox.checked;
onChange(checkbox.checked);
localStorage.setItem(checkboxId, checkbox.checked);
}
});
return container;
}
createCheckboxOption(labelText, isChecked = false, elementDepend, onChange) {
const container = this.createElement('div', {
display: 'flex',
alignItems: 'center',
margin: '5px 0',
cursor: 'pointer',
userSelect: 'none',
});
const checkboxId = `jvcimageviwer-checkbox-${labelText.replace(/\s+/g, '-').toLowerCase()}`;
// Vérifier si une valeur est stockée dans le localStorage
const storedValue = localStorage.getItem(checkboxId);
if (storedValue !== null) {
isChecked = (storedValue === "true");
}
// Gestion de l'affichage de l'élément dépendant
if (elementDepend) {
elementDepend.style.display = isChecked ? 'block' : 'none';
isChecked ? elementDepend.removeAttribute('data-hidden-by-options')
: elementDepend.setAttribute('data-hidden-by-options', 'true');
}
const checkbox = this.createElement('input');
checkbox.setAttribute('type', 'checkbox');
checkbox.checked = isChecked;
checkbox.setAttribute('id', checkboxId);
// Écouteur de changement sur la checkbox
checkbox.addEventListener('change', (e) => {
const isChecked = e.target.checked;
localStorage.setItem(checkboxId, isChecked);
if (typeof onChange === 'function') {
onChange(isChecked);
}
if (elementDepend) {
elementDepend.style.display = isChecked ? 'block' : 'none';
}
});
const label = this.createElement('label');
label.textContent = labelText;
label.setAttribute('for', checkboxId);
label.style.marginLeft = '10px';
container.append(checkbox, label);
// Ajout d'un écouteur d'événement sur le conteneur pour activer la case à cocher
container.addEventListener('click', (event) => {
if (event.target !== checkbox && event.target !== label) {
checkbox.checked = !checkbox.checked;
localStorage.setItem(checkboxId, checkbox.checked);
if (typeof onChange === 'function') {
onChange(checkbox.checked);
}
if (elementDepend) {
elementDepend.style.display = checkbox.checked ? 'block' : 'none';
}
}
});
return container;
}
createPlayPauseButton() {
const isMobileDevice = isMobile();
const button = this.createElement('button', {
backgroundColor: 'rgba(0, 0, 0, 0.6)',
color: 'white',
border: 'none',
borderRadius: '50%',
fontSize: isMobileDevice ? '12px' : '10px',
width: '37px',
height: '37px',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
position: 'relative',
zIndex: 10001,
backgroundImage: 'radial-gradient(circle, transparent 50%, rgba(255, 255, 255, 0.6) 50%)', // Ajout du dégradé radial
backgroundSize: '200% 200%',
});
button.style.backgroundImage = 'linear-gradient(to right, rgba(255,255,255,0.6), rgba(255,255,255,0.6))'; // Couleur unie pour le balayage
button.style.backgroundSize = '0% 100%'; // Initialement, la barre est invisible
button.style.backgroundRepeat = 'no-repeat'; // Empêche la répétition du gradient
// Conteneur pour l'icône SVG
const iconContainer = this.createElement('div', {
position: 'relative',
zIndex: 1, // SVG au-dessus de l'animation
});
button.appendChild(iconContainer);
button.iconContainer = iconContainer;
// Initialiser l'icône play/pause
this.updatePlayPauseButtonIcon(button);
button.addEventListener('click', () => {
this.togglePlayPause();
});
//this.addButtonEffects(button);
return button;
}
createFullScreenButton() {
const isMobileDevice = isMobile();
const button = this.createElement('button', {
position: 'relative',
backgroundColor: 'rgba(0, 0, 0, 0.6)',
color: 'white',
border: 'none',
borderRadius: '50%',
fontSize: isMobileDevice ? '12px' : '10px',
padding: '0',
cursor: 'pointer',
zIndex: 10001,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '37px',
height: '37px',
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.5)',
transition: 'transform 0.3s ease, background-color 0.3s ease',
});
button.setAttribute('title', 'Plein écran');
// SVG pour l'icône plein écran (vous pouvez le personnaliser)
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('width', isMobileDevice ? '18' : '22');
svg.setAttribute('height', isMobileDevice ? '18' : '22');
svg.setAttribute('fill', 'currentColor');
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', 'M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z'); // Icône plein écran
svg.appendChild(path);
button.appendChild(svg);
button.addEventListener('click', () => this.toggleFullScreen());
this.addButtonEffects(button);
return button;
}
toggleFullScreen() {
if (document.fullscreenElement) {
document.exitFullscreen();
} else {
this.imgElement.requestFullscreen(); // Met l'image en plein écran
}
}
createThumbnailPannel() {
const thumbnailPanel = this.createElement('div', {
position: 'fixed',
bottom: '10px',
left: '50%',
transform: 'translateX(-50%)',
border: '0px solid',
padding: '0px',
zIndex: '1010',
maxHeight: '80px',
maxWidth: '80%',
overflowY: 'hidden',
overflowX: 'auto',
display: 'flex',
alignItems: 'center',
backgroundColor: 'transparent',
});
thumbnailPanel.classList.add('thumbnail-scroll-container');
return thumbnailPanel;
}
addButtonEffects(button) {
button.addEventListener('mouseenter', () => {
button.style.backgroundColor = 'rgba(255, 255, 255, 0.8)';
button.style.color = 'black';
button.style.transform = 'scale(1.1)';
});
button.addEventListener('mouseleave', () => {
button.style.backgroundColor = 'rgba(0, 0, 0, 0.6)';
button.style.color = 'white';
button.style.transform = 'scale(1)';
});
button.addEventListener('mousedown', () => {
button.style.transform = 'scale(0.9)';
});
button.addEventListener('mouseup', () => {
button.style.transform = 'scale(1.1)';
});
}
toggleMenuOptions() {
if (this.optionsMenu.style.display === 'none') {
this.optionsMenu.style.display = 'flex';
} else {
this.optionsMenu.style.display = 'none';
}
}
// Ajoute les événements aux différents éléments du visualiseur
addEventListeners() {
// Bouttons de controles du visualiseur
this.prevButton.addEventListener('click', () => this.changeImage(-1));
this.nextButton.addEventListener('click', () => this.changeImage(1));
this.closeButton.addEventListener('click', () => this.closeViewer());
this.downloadButton.addEventListener('click', () => this.startDownload());
this.optionButton.addEventListener('click', () => this.toggleMenuOptions());
this.overlay.addEventListener('click', (event) => {
if ((!this.panzoom || !this.panzoom.isBusy()) && event.target === this.overlay) {
event.preventDefault();
event.stopPropagation();
this.closeViewer();
}
});
// Touches du clavier
document.addEventListener('keydown', (event) => this.handleKeyboardEvents(event));
}
// Fonctions pour gérer les touches du clavier
handleKeyboardEvents(event) {
switch (event.key) {
case 'ArrowLeft':
case 'ArrowUp':
this.changeImage(-1);
break;
case 'ArrowRight':
case 'ArrowDown':
this.changeImage(1);
break;
case 'Escape':
event.preventDefault();
this.closeViewer();
break;
}
}
// Met à jour l'image affichée dans le visualiseur
updateImage() {
if (this.currentIndex >= 0 && this.currentIndex < this.images.length) {
const imageUrl = this.images[this.currentIndex].href;
this.imgElement.src = imageUrl;
this.infoText.textContent = `${this.currentIndex + 1} / ${this.images.length}`;
this.spinner.style.display = 'block';
this.toggleButtonState();
this.imgElement.onload = () => {
this.imgElement.style.transition = 'opacity 0.2s ease-in-out'; // Transition pour l'opacité
this.imgElement.style.opacity = 1;
this.spinner.style.display = 'none';
this.imgElement.style.objectFit = 'contain';
// Réinitialiser le zoom et la position du drag
// this.panzoom.resetZoom();
// this.panzoom.resetDrag();
// Calcul de la position des boutons
this.focusOnThumbnail();
requestAnimationFrame(() => { // Ensures layout is calculated
this.imgContainer.style.maxWidth = '90%';
this.imgContainer.style.maxHeight = '80%';
this.imgElement.style.objectFit = 'contain';
// Set the zoom utils
this.initializePanzoom();
// Reset container height first (important!)
this.imgContainer.style.height = 'auto'; // or '0px' if you need an initial value
const displayedImageHeight = this.imgElement.offsetHeight;
const containerHeight = this.imgContainer.offsetHeight;
// Changed container height if image bigger than container
if (displayedImageHeight > containerHeight) {
this.imgContainer.style.transition = 'height 0.3s ease-in-out'; // Adjust duration and easing as needed
this.imgContainer.style.height = `${displayedImageHeight}px`;
} else {
// Remove transition if height is not changing (optional but recommended)
this.imgContainer.style.transition = 'none';
}
// Modify buttons Next/Prev position
const imgRect = this.imgElement.getBoundingClientRect();
const isMobileDevice = isMobile(); // Détection des mobiles
if (imgRect.width > this.defaultImageWidth) {
this.defaultImageWidth = imgRect.width; // Default width value for button space
}
if (isMobileDevice) {
// pass
} else {
this.calculateButtonPositions(this.defaultImageWidth);
}
});
};
this.imgElement.onerror = () => this.handleImageError();
}
}
updateContainerDimensions() {
// Force reflow
this.imgElement.offsetHeight;
//const imgRect = targetImg.getBoundingClientRect();
const imgWidth = this.imgElement.offsetWidth;
const imgHeight = this.imgElement.offsetHeight;
// Calculer la taille du conteneur en fonction des dimensions de l'image
const maxHeight = window.innerHeight;
const height = Math.min(imgHeight, maxHeight);
this.imgContainer.style.height = `${height}px`;
//this.imgContainer.style.border = "4px solid red";
}
// Gestion des erreurs de chargement d'image
handleImageError() {
const miniUrl = this.images[this.currentIndex].querySelector('img').src;
const fullUrl = this.images[this.currentIndex].href;
const extensions = this.reorderExtensions(fullUrl);
const baseUrl = miniUrl.replace('/minis/', '/fichiers/');
const tryNextExtension = (index) => {
if (index >= extensions.length) {
// Si toutes les extensions échouent, tenter l'URL originale (mini)
const imgTestMini = new Image();
imgTestMini.src = miniUrl;
imgTestMini.onload = () => {
this.imgElement.src = miniUrl;
};
imgTestMini.onerror = () => {
this.setImageNotFound(this.imgElement); // si même l'url mini marche pas afficher logo IMAGE NOT FOUND
};
return;
}
// Remplacer l'extension et mettre à jour l'URL
const updatedUrl = baseUrl.replace(/\.(jpg|png|jpeg|webp|gif)$/, extensions[index]);
// Tester l'URL avec un élément Image temporaire
const imgTest = new Image();
imgTest.src = updatedUrl;
imgTest.onload = () => {
this.imgElement.src = updatedUrl;
};
imgTest.onerror = () => {
// console.log("Error loading: " + updatedUrl);
tryNextExtension(index + 1);
};
};
// Commencer les essais avec la première extension
tryNextExtension(0);
}
calculateButtonPositions(imageWidth) { // Calculate button positions based on image width
const margin = 30;
// Calcul de la position des boutons
let prevButtonLeft = (window.innerWidth - imageWidth) / 2 - this.prevButton.offsetWidth - margin;
let nextButtonRight = (window.innerWidth - imageWidth) / 2 - this.nextButton.offsetWidth - margin;
// Limite les boutons pour qu'ils ne sortent pas de l'écran à gauche ou à droite
prevButtonLeft = Math.max(prevButtonLeft, margin);
nextButtonRight = Math.max(nextButtonRight, margin);
// Appliquer les positions ajustées
this.prevButton.style.left = `${prevButtonLeft}px`;
this.nextButton.style.right = `${nextButtonRight}px`;
}
initializePanzoom() {
if (!this.panzoom) {
this.panzoom = new Panzoom(this.imgElement, {
showPreviousImage: this.showPreviousImage.bind(this),
showNextImage: this.showNextImage.bind(this),
closeViewer: this.closeViewer.bind(this)
});
} else {
this.panzoom.reset(); // Reset existing Panzoom instance
}
}
setImageNotFound(imageElement) {
const notFoundImageUrl = "https://upload.wikimedia.org/wikipedia/commons/a/ac/No_image_available.svg";
imageElement.src = notFoundImageUrl;
}
// Réarranger la liste des extensions a tester pour mettre l'extension utilisée sur noelshack en premier
reorderExtensions(currentImageUrl) {
const extensions = ['.jpg', '.png', '.jpeg'];
const currentExtension = getImageExtension(currentImageUrl);
const newExtensions = [...extensions];
if (currentExtension) {
if (!newExtensions.includes(currentExtension)) {
newExtensions.unshift(currentExtension);
} else {
const index = newExtensions.indexOf(currentExtension);
if (index > -1) {
newExtensions.splice(index, 1);
newExtensions.unshift(currentExtension);
}
}
}
return newExtensions;
}
// Change d'image en fonction de la direction (suivant/précédent)
changeImage(direction) {
this.currentIndex = (this.currentIndex + direction + this.images.length) % this.images.length;
this.imgElement.style.transition = 'opacity 0.2s ease-in-out'; // Transition pour l'opacité
this.imgElement.style.opacity = 0;
this.spinner.style.display = 'block';
this.updateImage();
}
showPreviousImage() {
this.changeImage(-1);
}
showNextImage() {
this.changeImage(1);
}
// Désactive ou active les boutons suivant/précédent en fonction de l'index actuel
toggleButtonState() {
if (this.currentIndex === 0) {
// this.prevButton.disabled = true;
this.prevButton.style.opacity = 0.5;
this.prevButton.style.cursor = 'initial';
} else {
// this.prevButton.disabled = false;
this.prevButton.style.opacity = 1;
this.prevButton.style.cursor = 'pointer';
}
if (this.currentIndex === this.images.length - 1) {
// this.nextButton.disabled = true;
this.nextButton.style.opacity = 0.5;
this.nextButton.style.cursor = 'initial';
} else {
// this.nextButton.disabled = false;
this.nextButton.style.opacity = 1;
this.nextButton.style.cursor = 'pointer';
}
}
// Cacher temporairement le menu JVC
toggleMenuVisibility(isVisible) {
const menu = document.querySelector('.header__bottom');
if (menu) {
menu.style.display = isVisible ? '' : 'none';
}
}
focusOnThumbnail() {
const thumbnails = this.thumbnailPanel ? this.thumbnailPanel.querySelectorAll('img') : [];
const currentThumbnail = thumbnails[this.currentIndex];
if (this.previousThumbnail == currentThumbnail) {
return;
}
// Réinitialiser les styles de la miniature précédente en supprimant les classes
if (this.previousThumbnail) {
this.previousThumbnail.classList.remove('thumbnail-focus');
this.previousThumbnail.classList.add('thumbnail-reset');
}
// Ajouter des effets à la miniature actuelle en ajoutant la classe 'thumbnail-focus'
if (currentThumbnail) {
currentThumbnail.classList.remove('thumbnail-reset');
currentThumbnail.classList.add('thumbnail-focus');
currentThumbnail.parentElement.scrollIntoView({ behavior: 'smooth', inline: 'center' });
}
// Mettre à jour la référence de la miniature précédente
this.previousThumbnail = currentThumbnail;
}
// Fonction pour créer et afficher le panneau des miniatures
toggleThumbnailPanel() {
/*if (this.thumbnailPanel) {
this.closeThumbnailPanel(); // Ferme le panneau si déjà ouvert
return;
}*/
// Créer le panneau
if(this.thumbnailPanel == null) {
this.thumbnailPanel = this.createThumbnailPannel();
}
// Conteneur pour le défilement horizontal
const scrollContainer = this.createElement('div', {
display: 'flex',
overflowX: 'auto',
whiteSpace: 'nowrap',
maxWidth: '100%',
});
scrollContainer.classList.add('thumbnail-scroll-container');
// Ajout des images au conteneur
this.images.forEach((image, index) => {
const imgContainer = this.createElement('div', {
display: 'inline-block',
width: '50px',
height: '50px',
margin: '5px 2px',
padding: '4px 0px',
transition: 'transform 0.3s',
});
const imgThumbElement = this.createElement('img');
imgThumbElement.src = image.querySelector('img') ? image.querySelector('img').src : image.href || image.thumbnail;
imgThumbElement.onerror = () => {
this.setImageNotFound(imgThumbElement);
};
imgThumbElement.alt = `Image ${index + 1}`;
imgThumbElement.style.width = '50px';
imgThumbElement.style.height = '100%';
imgThumbElement.style.objectFit = 'cover';
imgThumbElement.style.cursor = 'pointer';
imgThumbElement.addEventListener('click', () => {
this.images.forEach((_, i) => {
const container = scrollContainer.children[i];
container.querySelector('img').style.border = 'none';
});
//imgElement.style.border = '2px solid blue';
this.currentIndex = index;
this.updateImage();
//imgContainer.scrollIntoView({ behavior: 'smooth', inline: 'center' });
});
imgContainer.appendChild(imgThumbElement);
scrollContainer.appendChild(imgContainer);
});
this.thumbnailPanel.appendChild(scrollContainer);
this.overlay.appendChild(this.thumbnailPanel);
this.focusOnThumbnail();
}
togglePlayPause() {
this.isPlaying = !this.isPlaying;
this.updatePlayPauseButtonIcon(this.playPauseButton);
if (this.isPlaying) {
this.startSlideshow();
} else {
this.stopSlideshow();
}
}
startSlideshow() {
this.isPlaying = true;
this.updatePlayPauseButtonIcon(this.playPauseButton);
this.animateProgressFill();
this.slideshowInterval = setInterval(() => {
this.changeImage(1);
this.animateProgressFill();
}, 5000); // 5 secondes d'intervalle
}
stopSlideshow() {
clearInterval(this.slideshowInterval);
this.slideshowInterval = null;
this.isPlaying = false;
this.updatePlayPauseButtonIcon(this.playPauseButton);
// Réinitialiser la barre de progression
this.playPauseButton.style.animation = 'none';
this.playPauseButton.style.backgroundSize = '0% 100%';
}
animateProgressFill() {
this.playPauseButton.style.animation = 'none';
void this.playPauseButton.offsetWidth;
this.playPauseButton.style.animation = 'progressFill 5s linear forwards'; // forwards pour que l'animation reste à 100%
}
updatePlayPauseButtonIcon(button) {
button.iconContainer.innerHTML = '';
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('width', '20');
svg.setAttribute('height', '20');
svg.setAttribute('fill', 'white');
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', this.isPlaying ? 'M6 19h4V5H6v14zm8-14v14h4V5h-4z' : 'M8 5v14l11-7z');
svg.appendChild(path);
button.iconContainer.appendChild(svg);
}
// Ecouteurs d'événements pour réinitialiser le timer
addInteractionListeners() {
this.overlay.addEventListener('mousemove', this.resetHideButtons.bind(this));
this.overlay.addEventListener('click', this.resetHideButtons.bind(this));
this.overlay.addEventListener('touchstart', this.resetHideButtons.bind(this));
this.imgElement.addEventListener('touchstart', this.resetHideButtons.bind(this));
}
// Réinitialisez le timer pour cacher les boutons
resetHideButtons() {
if (this.hideButtonsTimeout) {
clearTimeout(this.hideButtonsTimeout);
}
this.toggleButtonsVisibility(true);
// Si option masquer boutons desactiver quitter ici
if(this.freezeButton && !this.freezeButton.hasAttribute('data-hidden-by-options')){
return;
}
// Sinon appliquer le masquage automatique
this.hideButtonsTimeout = setTimeout(() => {
this.toggleButtonsVisibility(false); // Cachez les boutons après 3 secondes
}, 2500);
}
// Changez la visibilité des boutons
toggleButtonsVisibility(visible) {
const displayValue = visible ? 'flex' : 'none';
const elements = [
this.prevButton,
this.nextButton,
this.thumbnailPanel,
this.infoText,
this.downloadButton,
this.searchButton,
this.playPauseButton,
this.fullScreenButton,
this.optionButton,
];
elements.forEach(element => {
// Vérifiez si l'élément a été masqué par le système d'options
if (element && element.hasAttribute('data-hidden-by-options')) {
// Si l'élément a été masqué par les options, ne pas le réafficher
element.style.display = 'none';
} else if (element){
element.style.display = displayValue;
}
});
/*if(!visible) {
this.optionsMenu.style.display = displayValue;
}*/
}
startDownload() {
this.downloadButton.classList.add('downloading'); // Ajout de la classe pour l'animation
this.downloadCurrentImage().then(() => {
// Retirer la classe après le téléchargement
this.downloadButton.classList.remove('downloading');
}).catch((error) => {
console.error('Download failed:', error);
this.downloadButton.classList.remove('downloading');
});
}
downloadCurrentImage() {
return new Promise((resolve, reject) => {
const imageElement = this.imgElement;
if (!imageElement) {
console.error('Image not found!');
reject('Image not found');
return;
}
const imageUrl = imageElement.src;
const fileNameWithExtension = imageUrl.split('/').pop();
const fileName = fileNameWithExtension.substring(0, fileNameWithExtension.lastIndexOf('.'));
// Utilisation de GM.xmlHttpRequest pour contourner CORS
GM.xmlHttpRequest({
method: "GET",
url: imageUrl,
responseType: "blob",
headers: {
'Accept': 'image/jpeg,image/png,image/gif,image/bmp,image/tiff,image/*;q=0.8'
},
onload: function(response) {
if (response.status === 200) {
const blob = response.response;
const url = URL.createObjectURL(blob);
// Téléchargement du fichier
const a = document.createElement('a');
a.href = url;
a.download = fileName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
resolve(); // Indique que le téléchargement est terminé
} else {
reject('Error downloading image: ' + response.statusText);
}
},
onerror: function(err) {
reject('Request failed: ' + err);
}
});
});
}
searchImageOnGoogle() {
if (this.images.length > 0) {
const imageUrl = this.imgElement.src;
const googleImageSearchUrl = `https://lens.google.com/uploadbyurl?url=${encodeURIComponent(imageUrl)}`;
// Ouvrir le lien dans un nouvel onglet
window.open(googleImageSearchUrl, '_blank');
} else {
console.error('Aucune image disponible pour la recherche.');
}
}
disableScroll() {
document.body.style.overflow = 'hidden';
}
enableScroll() {
document.body.style.overflow = '';
}
// Fonction pour fermer le panneau des miniatures
closeThumbnailPanel(thumbnailPanel) {
if (this.thumbnailPanel && this.overlay.contains(this.thumbnailPanel)) {
this.overlay.removeChild(this.thumbnailPanel);
this.thumbnailPanel = null;
}
}
closeViewer() {
if (this.overlay) {
this.handleCloseViewer(); // Ferme le visualiseur
history.back(); // Supprime l'état ajouté par pushState
}
}
// Ferme le visualiseur d'images
handleCloseViewer() {
if (this.overlay) {
document.body.removeChild(this.overlay);
// Ferme le panneau des miniatures si ouvert
if (this.thumbnailPanel) {
this.closeThumbnailPanel(this.thumbnailPanel);
}
window.removeEventListener('popstate', this.handlePopState);
this.overlay = null;
this.isViewerOpen = false;
ImageViewer.instance = null; // Réinitialise l'instance singleton
this.toggleMenuVisibility(true);
}
this.enableScroll();
}
openViewer(images, currentIndex) {
if (this.overlay) {
this.images = images;
this.currentIndex = currentIndex;
this.updateImage();
this.toggleThumbnailPanel();
} else {
new ImageViewer();
this.images = images;
this.currentIndex = currentIndex;
this.createOverlay();
this.updateImage();
this.toggleThumbnailPanel();
}
this.isViewerOpen = true;
this.addHistoryState()
window.addEventListener('popstate', this.handlePopState); // Ecouter l'événement bouton back du navigateur
this.toggleMenuVisibility(false);
this.disableScroll();
}
handlePopState(event) {
if (ImageViewer.instance) {
event.preventDefault();
this.handleCloseViewer();
}
}
// Ajouter une entrée dans l'historique
addHistoryState() {
history.pushState({ viewerOpen: true }, '');
}
}
class StyleInjector {
constructor() {
this.styleElement = document.createElement('style');
document.head.appendChild(this.styleElement);
this.isMobileDevice = isMobile();
}
addSpinnerStyles() {
this.styleElement.textContent += `
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.spinner {
width: 50px;
height: 50px;
border-radius: 50%;
border: 5px solid transparent;
border-top: 5px solid rgba(0, 0, 0, 0.1);
background: conic-gradient(from 0deg, rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0));
animation: spin 1s linear infinite;
}
`;
}
addDownloadButtonStyles() {
this.styleElement.textContent += `
@keyframes rotate {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.downloading {
animation: rotate 1s linear infinite;
background-color: rgba(0, 0, 0, 0.8);
border-color: rgba(255, 255, 255, 0.5);
opacity: 0.7;
}
`;
}
addScrollBarStyles() {
if (!this.isMobileDevice) {
this.styleElement.textContent += `
.thumbnail-scroll-container::-webkit-scrollbar {
height: 7px;
background-color: transparent;
}
.thumbnail-scroll-container::-webkit-scrollbar-button {
display: none;
}
.thumbnail-scroll-container::-webkit-scrollbar-corner {
background-color: transparent;
}
.thumbnail-scroll-container::-webkit-scrollbar-thumb {
background-color: rgba(74, 77, 82, 0.7);
border: 2px solid transparent;
border-radius: 10px;
}
.thumbnail-scroll-container::-webkit-scrollbar-thumb:hover {
background-color: rgb(90, 93, 98, 0.7);
}
`;
}
}
addPlayPauseStyles() {
this.styleElement.textContent += `
@keyframes progressFill {
0% { background-size: 0% 100%; }
100% { background-size: 100% 100%; }
}
`;
}
addThumbnailStyles() {
this.styleElement.textContent += `
.thumbnail-focus {
transition: transform 0.4s ease, box-shadow 0.4s ease, filter 0.4s ease;
transform: scale(1.3);
filter: brightness(1.15);
z-index: 10;
position: relative;
border-radius: 2px;
}
.thumbnail-reset {
border: none;
transform: scale(1);
box-shadow: none;
filter: none;
z-index: 1;
position: relative;
border-radius: 0px;
}
`;
}
injectAllStyles() {
this.addSpinnerStyles();
this.addDownloadButtonStyles();
this.addPlayPauseStyles();
this.addThumbnailStyles();
this.addScrollBarStyles();
}
}
function injectStyles() {
const styleInjector = new StyleInjector();
styleInjector.injectAllStyles();
}
const parentClasses = `
.txt-msg,
.message,
.conteneur-message.mb-3,
.bloc-editor-forum,
.signature-msg,
.previsu-editor,
.bloc-description-desc.txt-enrichi-desc-profil,
.bloc-signature-desc.txt-enrichi-desc-profil
`.replace(/\s+/g, ''); // Supprimer les sauts de ligne et espaces inutiles
const linkSelectors = parentClasses.split(',').map(cls => `${cls} a`);
// Ajouter des écouteurs d'événements aux images sur la page
function addListeners() {
linkSelectors.forEach(selector => {
document.querySelectorAll(selector).forEach(link => {
link.addEventListener('click', handleImageClick, true);
});
});
}
function handleImageClick(event) {
// Si Ctrl ou Cmd est enfoncé, ne pas ouvrir l'ImageViewer
if (event.ctrlKey || event.metaKey) {
return;
}
const imgElement = this.querySelector('img');
if (imgElement) {
event.preventDefault();
const closestElement = this.closest(parentClasses);
if (closestElement) {
const images = Array.from(closestElement.querySelectorAll('a')).filter(imgLink => imgLink.querySelector('img'));
const currentIndex = images.indexOf(this);
const viewer = new ImageViewer();
viewer.openViewer(images, currentIndex);
}
}
}
function isMobile() {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
}
function getImageExtension(url) {
const match = url.match(/\.(jpg|jpeg|png|gif|bmp|webp|tiff)$/i); // Regexp pour matcher les extensions d'images
return match ? match[0].toLowerCase() : null;
}
// Observer les changements dans le DOM
function observeDOMChanges() {
const observer = new MutationObserver(() => addListeners());
observer.observe(document, { childList: true, subtree: true });
}
// Détection des changements d'URL
function observeURLChanges() {
let lastUrl = window.location.href;
const urlObserver = new MutationObserver(() => {
if (lastUrl !== window.location.href) {
lastUrl = window.location.href;
addListeners();
}
});
urlObserver.observe(document, { subtree: true, childList: true });
}
function main() {
injectStyles();
addListeners();
observeDOMChanges();
observeURLChanges();
}
main();
})();