JVC_ImageViewer

Naviguer entre les images d'un post sous forme de slideshow en cliquant sur une image sans ouvrir NoelShack.

As of 2024-09-15. See the latest version.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         JVC_ImageViewer
// @namespace    http://tampermonkey.net/
// @version      1.35.41
// @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://jvarchive.com/forums/*
// @grant        none
// @run-at       document-end
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    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.zoomLevel = 1;
            this.isDragging = false;
            this.startX = 0;
            this.startY = 0;
            this.offsetX = 0;
            this.offsetY = 0;

            ImageViewer.instance = 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
            });

            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() {
            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.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() {
          if (this.overlay) {
                document.body.removeChild(this.overlay);
                this.overlay = null;
                ImageViewer.instance = null; // Réinitialise l'instance singleton
            }
        }

      // 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';
          }
      }

      openViewer(images, currentIndex) {
            if (this.overlay) {
                this.images = images;
                this.currentIndex = currentIndex;
                this.updateImage();
                //this.overlay.style.display = 'flex';
            } else {
                new ImageViewer();
                this.images = images;
                this.currentIndex = currentIndex;
                this.createOverlay();
                this.updateImage();
                //this.overlay.style.display = 'flex';
            }
        }
    }

    // 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', '.conteneur-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, .conteneur-message').querySelectorAll('a')).filter(imgLink => imgLink.querySelector('img'));
                            const currentIndex = images.indexOf(this);

                            const viewer = new ImageViewer();
                            viewer.openViewer(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', '.main-content']
          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();

})();