JVC_ImageViewer

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

目前為 2024-09-15 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name JVC_ImageViewer
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.36.3
  5. // @description Naviguer entre les images d'un post sous forme de slideshow en cliquant sur une image sans ouvrir NoelShack.
  6. // @author HulkDu92
  7. // @match https://*.jeuxvideo.com/forums/*
  8. // @match https://jvarchive.com/forums/*
  9. // @grant none
  10. // @run-at document-end
  11. // @license MIT
  12. // ==/UserScript==
  13.  
  14. (function() {
  15. 'use strict';
  16.  
  17. class ImageViewer {
  18. constructor() {
  19. if (ImageViewer.instance) {
  20. return ImageViewer.instance;
  21. }
  22.  
  23. this.images = [];
  24. this.currentIndex = 0;
  25. this.overlay = null;
  26. this.imgElement = null;
  27. this.spinner = null;
  28. this.prevButton = null;
  29. this.nextButton = null;
  30. this.closeButton = null;
  31. this.infoText = null;
  32. this.zoomLevel = 1;
  33. this.isDragging = false;
  34. this.startX = 0;
  35. this.startY = 0;
  36. this.offsetX = 0;
  37. this.offsetY = 0;
  38. this.xDown = null;
  39. this.yDown = null;
  40. this.initialDistance = null;
  41. this.startTouches = [];
  42. this.isSwiping = false;
  43. this.isScaling = false;
  44.  
  45. ImageViewer.instance = this;
  46.  
  47. this.createOverlay();
  48. this.updateImage();
  49. }
  50.  
  51. // Crée et configure les éléments du visualiseur d'images (overlay, boutons, texte d'information, etc.)
  52. createOverlay() {
  53. this.overlay = this.createElement('div', {
  54. position: 'fixed',
  55. top: 0,
  56. left: 0,
  57. width: '100%',
  58. height: '100%',
  59. backgroundColor: 'rgba(0, 0, 0, 0.8)',
  60. display: 'flex',
  61. alignItems: 'center',
  62. justifyContent: 'center',
  63. zIndex: 10000
  64. });
  65.  
  66. this.imgElement = this.createElement('img', {
  67. maxWidth: '90%',
  68. maxHeight: '80%',
  69. objectFit: 'contain',
  70. transition: 'opacity 0.3s',
  71. opacity: 0,
  72. cursor: 'pointer'
  73. });
  74.  
  75. this.spinner = this.createSpinner();
  76. this.prevButton = this.createButton('<', 'left');
  77. this.nextButton = this.createButton('>', 'right');
  78. this.closeButton = this.createCloseButton();
  79. this.infoText = this.createInfoText();
  80.  
  81. // Événements associés aux boutons et à l'overlay
  82. this.addEventListeners();
  83.  
  84. // Ajout des éléments au DOM
  85. this.overlay.append(this.imgElement, this.spinner, this.prevButton, this.nextButton, this.closeButton, this.infoText);
  86. document.body.appendChild(this.overlay);
  87. }
  88.  
  89. // Crée un élément HTML avec des styles
  90. createElement(tag, styles = {}) {
  91. const element = document.createElement(tag);
  92. Object.assign(element.style, styles);
  93. return element;
  94. }
  95.  
  96. // Crée le bouton précédent ou suivant
  97. createButton(text, position) {
  98. const button = this.createElement('button', {
  99. position: 'absolute',
  100. [position]: '10px',
  101. backgroundColor: 'rgba(0, 0, 0, 0.6)',
  102. color: 'white',
  103. fontSize: '22px',
  104. border: 'none',
  105. borderRadius: '50%',
  106. width: '40px',
  107. height: '40px',
  108. cursor: 'pointer',
  109. display: 'flex',
  110. alignItems: 'center',
  111. justifyContent: 'center',
  112. boxShadow: '0px 4px 8px rgba(0, 0, 0, 0.6)',
  113. transition: 'background-color 0.3s, transform 0.3s'
  114. });
  115.  
  116. button.textContent = text;
  117. this.addButtonEffects(button);
  118.  
  119. return button;
  120. }
  121.  
  122. // Crée le bouton de fermeture
  123. createCloseButton() {
  124. const button = this.createElement('button', {
  125. position: 'absolute',
  126. top: '80px',
  127. right: '10px',
  128. backgroundColor: 'rgba(0, 0, 0, 0.8)',
  129. color: 'white',
  130. fontSize: '14px',
  131. border: 'none',
  132. borderRadius: '50%',
  133. width: '35px',
  134. height: '35px',
  135. cursor: 'pointer',
  136. zIndex: 10001
  137. });
  138.  
  139. button.textContent = '✕';
  140. this.addButtonEffects(button);
  141.  
  142. return button;
  143. }
  144.  
  145. // Crée la zone d'affichage du texte d'information (numéro d'image)
  146. createInfoText() {
  147. return this.createElement('div', {
  148. position: 'absolute',
  149. bottom: '10px',
  150. right: '10px',
  151. color: 'white',
  152. fontSize: '16px',
  153. backgroundColor: 'rgba(0, 0, 0, 0.5)',
  154. padding: '5px',
  155. borderRadius: '5px',
  156. zIndex: 10001
  157. });
  158. }
  159.  
  160. // Crée un spinner pour indiquer le chargement de l'image
  161. createSpinner() {
  162. const spinner = this.createElement('div', {
  163. position: 'absolute',
  164. border: '8px solid #f3f3f3',
  165. borderTop: '8px solid #3498db',
  166. borderRadius: '50%',
  167. width: '50px',
  168. height: '50px',
  169. animation: 'spin 1s linear infinite',
  170. zIndex: 10001
  171. });
  172. return spinner;
  173. }
  174.  
  175. addButtonEffects(button) {
  176. button.addEventListener('mouseenter', () => {
  177. button.style.backgroundColor = 'rgba(255, 255, 255, 0.8)';
  178. button.style.color = 'black';
  179. button.style.transform = 'scale(1.1)';
  180. });
  181.  
  182. button.addEventListener('mouseleave', () => {
  183. button.style.backgroundColor = 'rgba(0, 0, 0, 0.6)';
  184. button.style.color = 'white';
  185. button.style.transform = 'scale(1)';
  186. });
  187.  
  188. button.addEventListener('mousedown', () => {
  189. button.style.transform = 'scale(0.9)';
  190. });
  191.  
  192. button.addEventListener('mouseup', () => {
  193. button.style.transform = 'scale(1.1)';
  194. });
  195. }
  196.  
  197. // Ajoute les événements aux différents éléments du visualiseur
  198. addEventListeners() {
  199. console.log("addEventListeners");
  200. this.prevButton.addEventListener('click', () => this.changeImage(-1));
  201. this.nextButton.addEventListener('click', () => this.changeImage(1));
  202. this.closeButton.addEventListener('click', () => this.closeViewer());
  203. this.overlay.addEventListener('click', (event) => {
  204. if (event.target === this.overlay) {
  205. this.closeViewer();
  206. }
  207. });
  208.  
  209. // Zoom avec la molette de la souris
  210. this.imgElement.addEventListener('wheel', (event) => this.handleZoom(event));
  211.  
  212. // Déplacement lors du zoom (drag)
  213. this.imgElement.addEventListener('mousedown', (event) => this.startDrag(event));
  214. document.addEventListener('mousemove', (event) => this.onDrag(event));
  215. document.addEventListener('mouseup', () => this.endDrag());
  216.  
  217.  
  218. // Glissement sur mobile (swipe)
  219. this.overlay.addEventListener('touchstart', (event) => this.handleTouchStart(event));
  220. this.overlay.addEventListener('touchmove', (event) => this.handleTouchMove(event));
  221. this.overlay.addEventListener('touchend', (event) => this.handleTouchEnd(event));
  222.  
  223. // Zoom sur mobile
  224. this.overlay.addEventListener('touchstart', (event) => this.handlePinchStart(event));
  225. this.overlay.addEventListener('touchmove', (event) => this.handlePinchMove(event));
  226. this.overlay.addEventListener('touchend', (event) => this.handlePinchEnd(event));
  227.  
  228.  
  229. this.imgElement.addEventListener('click', () => {
  230. window.open(this.images[this.currentIndex].href, '_blank');
  231. });
  232. }
  233.  
  234. // Gestion du début de l'interaction tactile
  235. handleTouchStart(event) {
  236. if (event.touches.length === 1) {
  237. // Commencer le swipe
  238. this.isSwiping = true;
  239. this.startX = event.touches[0].clientX;
  240. } else if (event.touches.length === 2) {
  241. // Commencer le zoom par pincement
  242. this.startTouches = [...event.touches];
  243. this.initialDistance = this.getTouchDistance(this.startTouches);
  244. }
  245. }
  246.  
  247. // Gestion du mouvement tactile pour swipe ou zoom
  248. handleTouchMove(event) {
  249. if (this.isSwiping && event.touches.length === 1) {
  250. // Swipe
  251. this.currentX = event.touches[0].clientX;
  252. } else if (event.touches.length === 2) {
  253. // Zoom par pincement
  254. this.handlePinchZoom(event);
  255. }
  256. }
  257.  
  258. // Gestion de la fin de l'interaction tactile
  259. handleTouchEnd(event) {
  260. if (event.touches.length < 2) {
  261. this.initialDistance = null;
  262. }
  263. if (this.isSwiping) {
  264. this.isSwiping = false;
  265. const deltaX = this.currentX - this.startX;
  266.  
  267. // Si le mouvement est suffisamment grand, on change d'image
  268. if (Math.abs(deltaX) > 50) {
  269. if (deltaX > 0) {
  270. this.showPreviousImage(); // Fonction pour afficher l'image précédente
  271. } else {
  272. this.showNextImage(); // Fonction pour afficher l'image suivante
  273. }
  274. }
  275. }
  276. }
  277.  
  278. // Gestion du début de l'interaction tactile pour le pincement
  279. handlePinchStart(event) {
  280. if (event.touches.length === 2) {
  281. // On commence à zoomer
  282. this.initialDistance = this.getTouchDistance(event.touches);
  283. this.isScaling = true;
  284. }
  285. }
  286.  
  287. // Gestion du mouvement tactile pour le pincement (zoom)
  288. handlePinchMove(event) {
  289. if (this.isScaling && event.touches.length === 2) {
  290. console.log("moving the zoom");
  291. const newDistance = this.getTouchDistance(event.touches);
  292. const scaleFactor = newDistance / this.initialDistance;
  293. this.zoomImage(scaleFactor);
  294. this.initialDistance = newDistance;
  295. }
  296. }
  297.  
  298. // Gestion de la fin de l'interaction tactile pour le pincement
  299. handlePinchEnd(event) {
  300. if (event.touches.length < 2) {
  301. this.isScaling = false;
  302. console.log("end of zoom pinch");
  303. }
  304. }
  305.  
  306. /*zoomImage(scaleFactor) {
  307. this.zoomLevel *= scaleFactor;
  308. this.imgElement.style.transform = `scale(${this.zoomLevel})`;
  309. }*/
  310.  
  311. zoomImage(scaleFactor) {
  312. console.log("zooming");
  313. // Calcul du nouveau niveau de zoom
  314. this.zoomLevel *= scaleFactor;
  315.  
  316. // Ajuster le zoomLevel pour ne pas descendre sous 1
  317. this.zoomLevel = Math.max(1, this.zoomLevel);
  318.  
  319. // Calculer la position du zoom basé sur le centre de l'image
  320. const rect = this.imgElement.getBoundingClientRect();
  321. const offsetX = (rect.width / 2 - (rect.width / 2 * scaleFactor)) / scaleFactor;
  322. const offsetY = (rect.height / 2 - (rect.height / 2 * scaleFactor)) / scaleFactor;
  323.  
  324. // Appliquer la transformation CSS
  325. this.imgElement.style.transform = `scale(${this.zoomLevel}) translate(${offsetX}px, ${offsetY}px)`;
  326. console.log("zoom done");
  327. console.log(this.imgElement)
  328. }
  329.  
  330.  
  331. // Fonction pour calculer la distance entre deux points de contact
  332. getTouchDistance(touches) {
  333. const [touch1, touch2] = touches;
  334. const deltaX = touch2.clientX - touch1.clientX;
  335. const deltaY = touch2.clientY - touch1.clientY;
  336. return Math.sqrt(deltaX * deltaX + deltaY * deltaY);
  337. }
  338.  
  339. handleZoom(event) {
  340. event.preventDefault();
  341. const zoomIncrement = 0.1;
  342.  
  343. if (event.deltaY < 0) {
  344. this.zoomLevel += zoomIncrement; // Zoomer
  345. } else {
  346. this.zoomLevel = Math.max(1, this.zoomLevel - zoomIncrement); // Dézoomer, mais ne pas descendre sous 1
  347. }
  348.  
  349. this.imgElement.style.transform = `scale(${this.zoomLevel}) translate(${this.offsetX}px, ${this.offsetY}px)`;
  350. }
  351.  
  352. startDrag(event) {
  353. this.isDragging = true;
  354. this.startX = event.clientX - this.offsetX;
  355. this.startY = event.clientY - this.offsetY;
  356. this.imgElement.style.cursor = 'grabbing';
  357. }
  358.  
  359. onDrag(event) {
  360. if (!this.isDragging) return;
  361.  
  362. this.offsetX = event.clientX - this.startX;
  363. this.offsetY = event.clientY - this.startY;
  364.  
  365. this.imgElement.style.transform = `scale(${this.zoomLevel}) translate(${this.offsetX}px, ${this.offsetY}px)`;
  366. }
  367.  
  368. endDrag() {
  369. this.isDragging = false;
  370. this.imgElement.style.cursor = 'grab';
  371. }
  372.  
  373. startTouchDrag(event) {
  374. if (event.touches.length === 1) { // S'assurer qu'il y a un seul doigt
  375. this.isDragging = true;
  376. this.startX = event.touches[0].clientX - this.offsetX;
  377. this.startY = event.touches[0].clientY - this.offsetY;
  378. }
  379. }
  380.  
  381. onTouchDrag(event) {
  382. if (!this.isDragging || event.touches.length !== 1) return;
  383.  
  384. this.offsetX = event.touches[0].clientX - this.startX;
  385. this.offsetY = event.touches[0].clientY - this.startY;
  386.  
  387. this.imgElement.style.transform = `scale(${this.zoomLevel}) translate(${this.offsetX}px, ${this.offsetY}px)`;
  388. }
  389.  
  390.  
  391. // Gestion du zoom pour appareils tactiles (pincement)
  392. handlePinchZoom(event) {
  393. this.zoomLevel += event.scale - 1; // Ajuste le zoom en fonction de l'échelle du pincement
  394. this.imgElement.style.transform = `scale(${Math.max(1, this.zoomLevel)})`; // Ne pas descendre en dessous d'un zoom de 1
  395. }
  396.  
  397. // Met à jour l'image affichée dans le visualiseur
  398. updateImage() {
  399. if (this.currentIndex >= 0 && this.currentIndex < this.images.length) {
  400. const imageUrl = this.images[this.currentIndex].href;
  401. this.imgElement.src = imageUrl;
  402. this.infoText.textContent = `${this.currentIndex + 1} / ${this.images.length}`;
  403. this.spinner.style.display = 'block';
  404.  
  405. this.toggleButtonState();
  406.  
  407. this.imgElement.onload = () => {
  408. this.imgElement.style.opacity = 1;
  409. this.spinner.style.display = 'none';
  410. };
  411.  
  412. this.imgElement.onerror = () => this.handleImageError();
  413. }
  414. }
  415.  
  416. // Gestion des erreurs de chargement d'image
  417. handleImageError() {
  418. const miniUrl = this.images[this.currentIndex].querySelector('img').src;
  419. const extensions = ['.jpg', '.png', '.jpeg'];
  420. const baseUrl = miniUrl.replace('/minis/', '/fichiers/');
  421.  
  422. const tryNextExtension = (index) => {
  423. if (index >= extensions.length) {
  424. // Si toutes les extensions échouent, tenter l'URL originale
  425. this.imgElement.src = miniUrl;
  426. return;
  427. }
  428.  
  429. // Remplacer l'extension et mettre à jour l'URL
  430. const updatedUrl = baseUrl.replace(/\.(jpg|png|jpeg)$/, extensions[index]);
  431. this.imgElement.src = updatedUrl;
  432.  
  433. // Tester l'URL
  434. this.imgElement.onerror = () => tryNextExtension(index + 1);
  435. };
  436.  
  437. // Commencer les essais avec la première extension
  438. tryNextExtension(0);
  439. }
  440.  
  441. // Change d'image en fonction de la direction (suivant/précédent)
  442. changeImage(direction) {
  443. this.currentIndex = (this.currentIndex + direction + this.images.length) % this.images.length;
  444. this.imgElement.style.opacity = 0;
  445. this.spinner.style.display = 'block';
  446. this.updateImage();
  447. }
  448.  
  449. showPreviousImage() {
  450. this.changeImage(-1);
  451. }
  452.  
  453. showNextImage() {
  454. this.changeImage(1);
  455. }
  456.  
  457. // Ferme le visualiseur d'images
  458. closeViewer() {
  459. if (this.overlay) {
  460. document.body.removeChild(this.overlay);
  461. this.overlay = null;
  462. ImageViewer.instance = null; // Réinitialise l'instance singleton
  463. }
  464. }
  465.  
  466. // Désactive ou active les boutons suivant/précédent en fonction de l'index actuel
  467. toggleButtonState() {
  468. if (this.currentIndex === 0) {
  469. this.prevButton.disabled = true;
  470. this.prevButton.style.opacity = 0.5;
  471. this.prevButton.style.cursor = 'initial';
  472. } else {
  473. this.prevButton.disabled = false;
  474. this.prevButton.style.opacity = 1;
  475. this.prevButton.style.cursor = 'pointer';
  476. }
  477.  
  478. if (this.currentIndex === this.images.length - 1) {
  479. this.nextButton.disabled = true;
  480. this.nextButton.style.opacity = 0.5;
  481. this.nextButton.style.cursor = 'initial';
  482. } else {
  483. this.nextButton.disabled = false;
  484. this.nextButton.style.opacity = 1;
  485. this.nextButton.style.cursor = 'pointer';
  486. }
  487. }
  488.  
  489. openViewer(images, currentIndex) {
  490. if (this.overlay) {
  491. this.images = images;
  492. this.currentIndex = currentIndex;
  493. this.updateImage();
  494. //this.overlay.style.display = 'flex';
  495. } else {
  496. new ImageViewer();
  497. this.images = images;
  498. this.currentIndex = currentIndex;
  499. this.createOverlay();
  500. this.updateImage();
  501. //this.overlay.style.display = 'flex';
  502. }
  503. }
  504. }
  505.  
  506. function addSpinnerStyles() {
  507. const style = document.createElement('style');
  508. style.textContent = `
  509. @keyframes spin {
  510. 0% { transform: rotate(0deg); }
  511. 100% { transform: rotate(360deg); }
  512. }
  513. .spinner { /* Exemple de classe pour spinner */
  514. width: 50px;
  515. height: 50px;
  516. border: 5px solid rgba(0, 0, 0, 0.1);
  517. border-left-color: #000;
  518. border-radius: 50%;
  519. animation: spin 1s linear infinite;
  520. }
  521. `;
  522. document.head.appendChild(style);
  523. }
  524.  
  525. const parentClasses = '.txt-msg, .message, .conteneur-message.mb-3, .bloc-editor-forum, .signature-msg';
  526. const linkSelectors = parentClasses.split(', ').map(cls => `${cls} a`);
  527.  
  528. // Ajouter des écouteurs d'événements aux images sur la page
  529. function addListeners() {
  530. linkSelectors.forEach(selector => {
  531. document.querySelectorAll(selector).forEach(link => {
  532. link.addEventListener('click', handleImageClick, true);
  533. });
  534. });
  535. }
  536.  
  537. function handleImageClick(event) {
  538. const imgElement = this.querySelector('img');
  539. if (imgElement) {
  540. event.preventDefault();
  541. const closestElement = this.closest(parentClasses);
  542. if (closestElement) {
  543. const images = Array.from(closestElement.querySelectorAll('a')).filter(imgLink => imgLink.querySelector('img'));
  544. const currentIndex = images.indexOf(this);
  545.  
  546. const viewer = new ImageViewer();
  547. viewer.openViewer(images, currentIndex);
  548. }
  549. }
  550. }
  551.  
  552. function main() {
  553. addSpinnerStyles();
  554. addListeners();
  555.  
  556. const observer = new MutationObserver(() => addListeners());
  557. observer.observe(document, { childList: true, subtree: true });
  558. }
  559.  
  560. main();
  561.  
  562.  
  563. })();