// ==UserScript==
// @name JVC_ImageViewer
// @namespace http://tampermonkey.net/
// @version 1.35.2
// @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/*
// @grant none
// @run-at document-end
// @license MIT
// ==/UserScript==
(function() {
'use strict';
class ImageViewer {
constructor(images, currentIndex) {
this.images = images;
this.currentIndex = currentIndex;
this.overlay = null;
this.imgElement = null;
this.spinner = null;
this.prevButton = null;
this.nextButton = null;
this.closeButton = null;
this.infoText = null;
this.zoomLevel = 1; // Niveau de zoom initial
this.isDragging = false; // Pour savoir si l'utilisateur est en train de glisser
this.startX = 0; // Position de départ sur l'axe X lors du glissement
this.startY = 0; // Position de départ sur l'axe Y lors du glissement
this.offsetX = 0; // Décalage de l'image sur l'axe X
this.offsetY = 0; // Décalage de l'image sur l'axe Y
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
});
this.imgElement = this.createElement('img', {
maxWidth: '90%',
maxHeight: '80%',
objectFit: 'contain',
transition: 'opacity 0.3s',
opacity: 0,
cursor: 'pointer'
});
this.spinner = this.createSpinner();
this.prevButton = this.createButton('<', 'left');
this.nextButton = this.createButton('>', 'right');
this.closeButton = this.createCloseButton();
this.infoText = this.createInfoText();
// Événements associés aux boutons et à l'overlay
this.addEventListeners();
// Ajout des éléments au DOM
this.overlay.append(this.imgElement, this.spinner, this.prevButton, this.nextButton, this.closeButton, this.infoText);
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 button = this.createElement('button', {
position: 'absolute',
[position]: '10px',
backgroundColor: 'rgba(0, 0, 0, 0.6)',
color: 'white',
fontSize: '22px',
border: 'none',
borderRadius: '50%',
width: '40px',
height: '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;
this.addButtonEffects(button);
return button;
}
// Crée le bouton de fermeture
createCloseButton() {
const button = this.createElement('button', {
position: 'absolute',
top: '80px',
right: '10px',
backgroundColor: 'rgba(0, 0, 0, 0.8)',
color: 'white',
fontSize: '14px',
border: 'none',
borderRadius: '50%',
width: '35px',
height: '35px',
cursor: 'pointer',
zIndex: 10001
});
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',
bottom: '10px',
right: '10px',
color: 'white',
fontSize: '16px',
backgroundColor: 'rgba(0, 0, 0, 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;
}
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)';
});
}
// Ajoute les événements aux différents éléments du visualiseur
addEventListeners() {
this.prevButton.addEventListener('click', () => this.changeImage(-1));
this.nextButton.addEventListener('click', () => this.changeImage(1));
this.closeButton.addEventListener('click', () => this.closeViewer());
this.overlay.addEventListener('click', (event) => {
if (event.target === this.overlay) {
this.closeViewer();
}
});
// Zoom avec la molette de la souris
this.imgElement.addEventListener('wheel', (event) => this.handleZoom(event));
// Déplacement lors du zoom (drag)
this.imgElement.addEventListener('mousedown', (event) => this.startDrag(event));
document.addEventListener('mousemove', (event) => this.onDrag(event));
document.addEventListener('mouseup', () => this.endDrag());
// Déplacement pour appareils tactiles (pincement et drag)
this.imgElement.addEventListener('touchstart', (event) => this.startTouchDrag(event));
this.imgElement.addEventListener('touchmove', (event) => this.onTouchDrag(event));
this.imgElement.addEventListener('touchend', () => this.endDrag());
this.imgElement.addEventListener('click', () => {
window.open(this.images[this.currentIndex].href, '_blank');
});
}
handleZoom(event) {
event.preventDefault();
const zoomIncrement = 0.1;
if (event.deltaY < 0) {
this.zoomLevel += zoomIncrement; // Zoomer
} else {
this.zoomLevel = Math.max(1, this.zoomLevel - zoomIncrement); // Dézoomer, mais ne pas descendre sous 1
}
this.imgElement.style.transform = `scale(${this.zoomLevel}) translate(${this.offsetX}px, ${this.offsetY}px)`;
}
startDrag(event) {
this.isDragging = true;
this.startX = event.clientX - this.offsetX;
this.startY = event.clientY - this.offsetY;
this.imgElement.style.cursor = 'grabbing';
}
onDrag(event) {
if (!this.isDragging) return;
this.offsetX = event.clientX - this.startX;
this.offsetY = event.clientY - this.startY;
this.imgElement.style.transform = `scale(${this.zoomLevel}) translate(${this.offsetX}px, ${this.offsetY}px)`;
}
endDrag() {
this.isDragging = false;
this.imgElement.style.cursor = 'grab';
}
startTouchDrag(event) {
if (event.touches.length === 1) { // S'assurer qu'il y a un seul doigt
this.isDragging = true;
this.startX = event.touches[0].clientX - this.offsetX;
this.startY = event.touches[0].clientY - this.offsetY;
}
}
onTouchDrag(event) {
if (!this.isDragging || event.touches.length !== 1) return;
this.offsetX = event.touches[0].clientX - this.startX;
this.offsetY = event.touches[0].clientY - this.startY;
this.imgElement.style.transform = `scale(${this.zoomLevel}) translate(${this.offsetX}px, ${this.offsetY}px)`;
}
// Gestion du zoom pour appareils tactiles (pincement)
handlePinchZoom(event) {
this.zoomLevel += event.scale - 1; // Ajuste le zoom en fonction de l'échelle du pincement
this.imgElement.style.transform = `scale(${Math.max(1, this.zoomLevel)})`; // Ne pas descendre en dessous d'un zoom de 1
}
// Met à jour l'image affichée dans le visualiseur
updateImage() {
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.opacity = 1;
this.spinner.style.display = 'none';
};
this.imgElement.onerror = () => this.handleImageError();
}
// Gestion des erreurs de chargement d'image
handleImageError() {
const miniUrl = this.images[this.currentIndex].querySelector('img').src;
const updatedUrl = miniUrl.replace('/minis/', '/fichiers/').replace('.png', '.jpg');
this.imgElement.src = updatedUrl;
this.imgElement.onerror = () => {
const secondTryUrl = miniUrl.replace('/minis/', '/fichiers/').replace('.jpg', '.png');
this.imgElement.src = secondTryUrl;
this.imgElement.onerror = () => {
this.imgElement.src = miniUrl;
};
};
}
// 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.opacity = 0;
this.spinner.style.display = 'block';
this.updateImage();
}
// Ferme le visualiseur d'images
closeViewer() {
document.body.removeChild(this.overlay);
}
// 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';
}
}
}
// Ajout des animations pour le spinner
const style = document.createElement('style');
style.textContent = `
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`;
document.head.appendChild(style);
// Ajoute des écouteurs d'événements aux images sur la page
function addListeners() {
const selectors = ['.txt-msg a', '.message a'];
selectors.forEach(selector => {
const links = document.querySelectorAll(selector);
if (links.length > 0) {
links.forEach(link => {
link.addEventListener('click', function(event) {
const imgElement = this.querySelector('img');
if (imgElement) {
event.preventDefault();
const images = Array.from(this.closest('.txt-msg, .message').querySelectorAll('a')).filter(imgLink => imgLink.querySelector('img'));
const currentIndex = images.indexOf(this);
new ImageViewer(images, currentIndex);
}
}, true);
});
}
});
}
function main(){
// Surveille les changements dans le DOM pour ajouter des événements aux nouveaux éléments
let observer;
try {
const selectors = ['#page-messages-forum', '.bloc-messages']
observer = new MutationObserver(() => addListeners());
const targetNode = document.querySelector(selectors);
const config = { childList: true, subtree: true };
if (targetNode) {
observer.observe(targetNode, config);
}
} catch (error) {
// console.error("Error setting up MutationObserver:", error);
}
// Ajoute les écouteurs d'événements initiaux
try {
addListeners();
} catch (error) {
// console.error("Error in initial addListeners call:", error);
}
}
main();
})();