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