JVC_ImageViewer

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

目前为 2024-10-24 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name JVC_ImageViewer
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.42.5
  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://*.jeuxvideo.com/profil/*
  9. // @match https://*.jeuxvideo.com/messages-prives/*
  10. // @match https://jvarchive.com/*
  11. // @grant GM_download
  12. // @grant GM.xmlHttpRequest
  13. // @connect image.noelshack.com
  14. // @run-at document-end
  15. // @license MIT
  16. // @icon https://image.noelshack.com/fichiers/2024/41/3/1728506420-image-viewer-icon.png
  17. // ==/UserScript==
  18.  
  19. (function() {
  20. 'use strict';
  21.  
  22. class ImageViewer {
  23. constructor() {
  24. if (ImageViewer.instance) {
  25. return ImageViewer.instance;
  26. }
  27.  
  28. this.images = [];
  29. this.currentIndex = 0;
  30. this.overlay = null;
  31. this.imgElement = null;
  32. this.spinner = null;
  33. this.prevButton = null;
  34. this.nextButton = null;
  35. this.closeButton = null;
  36. this.infoText = null;
  37. this.downloadButton = null;
  38. this.searchButton = null;
  39. this.optionButton = null;
  40. this.indicatorsContainer = null;
  41. this.indicators = [];
  42. this.zoomLevel = 1;
  43. this.isDragging = false;
  44. this.isPinchZooming = false;
  45. this.startX = 0;
  46. this.startY = 0;
  47. this.offsetX = 0;
  48. this.offsetY = 0;
  49. this.xDown = null;
  50. this.yDown = null;
  51. this.initialDistance = null;
  52. this.startTouches = [];
  53. this.isSwiping = false;
  54. this.isScaling = false;
  55. this.imageElementScale = 1;
  56. this.start = {};
  57. this.isMouseDown = false;
  58. this.isTouchDragging = false;
  59. this.dragTimeout = null;
  60. this.mouseDownX = 0;
  61. this.mouseDownY = 0;
  62. this.initialScale = 1;
  63. this.isViewerOpen = false;
  64. this.thumbnailPanel = null;
  65. this.previousThumbnail = null;
  66. this.touchSensitivityFactor = 0.5; // Pour les appareils tactiles
  67. this.mouseSensitivityFactor = 0.4; // Pour les mouvements de souris
  68. this.defaultImageWidth = Math.min(window.innerWidth, 1200);
  69.  
  70. ImageViewer.instance = this;
  71.  
  72. this.handlePopState = this.handlePopState.bind(this);
  73.  
  74. this.createOverlay();
  75. this.updateImage();
  76. }
  77.  
  78. // Crée et configure les éléments du visualiseur d'images (overlay, boutons, texte d'information, etc.)
  79. createOverlay() {
  80. this.overlay = this.createElement('div', {
  81. position: 'fixed',
  82. top: 0,
  83. left: 0,
  84. width: '100%',
  85. height: '100%',
  86. backgroundColor: 'rgba(0, 0, 0, 0.8)',
  87. display: 'flex',
  88. alignItems: 'center',
  89. justifyContent: 'center',
  90. zIndex: 10000
  91. });
  92. this.overlay.classList.add('https://www.jeuxvideo.com/forums/42-51-74793440-1-0-1-0-officiel-jeux-paralympiques-de-paris-2024-du-28-aout-au-8-septembre.htm');
  93.  
  94.  
  95. this.imgElement = this.createElement('img', {
  96. maxWidth: '90%',
  97. maxHeight: '80%',
  98. objectFit: 'contain',
  99. transition: 'opacity 0.3s',
  100. opacity: 0,
  101. cursor: 'pointer',
  102. overflow: 'auto',
  103. });
  104.  
  105. this.spinner = this.createSpinner();
  106. this.prevButton = this.createButton('<', 'left');
  107. this.nextButton = this.createButton('>', 'right');
  108. this.closeButton = this.createCloseButton();
  109. this.infoText = this.createInfoText();
  110.  
  111. this.downloadButton = this.createDownloadButton();
  112. this.searchButton = this.createSearchButton();
  113. this.optionButton = this.createOptionButton();
  114.  
  115. this.thumbnailPanel = this.createThumbnailPannel();
  116. this.optionsMenu = this.createOptionsMenu(); // must be last
  117.  
  118. this.indicatorsContainer = this.createElement('div', {
  119. display: 'flex',
  120. justifyContent: 'center',
  121. marginBottom: '10px 0',
  122. position: 'absolute',
  123. bottom: '40px',
  124. });
  125.  
  126. // Ajouter ici la logique de placement en bas à droite
  127. const buttonContainer = this.createElement('div', {
  128. position: 'absolute',
  129. bottom: '30px',
  130. right: '10px',
  131. display: 'flex',
  132. flexDirection: 'column',
  133. gap: '5px',
  134. zIndex: 10001,
  135. });
  136.  
  137. // Ajouter les boutons dans ce conteneur
  138. buttonContainer.append(this.searchButton, this.downloadButton, this.optionButton);
  139. this.overlay.append(
  140. this.imgElement,
  141. this.spinner,
  142. this.infoText,
  143. this.prevButton,
  144. this.nextButton,
  145. this.closeButton,
  146. buttonContainer, // Ajouter le conteneur de boutons à l'overlay
  147. this.indicatorsContainer
  148. );
  149.  
  150. // Positionner le menu d'options à gauche de buttonContainer
  151. this.overlay.append(this.optionsMenu);
  152.  
  153. // Événements associés aux boutons et à l'overlay
  154. this.resetHideButtons();
  155. this.addEventListeners();
  156. this.addInteractionListeners();
  157.  
  158. document.body.appendChild(this.overlay);
  159. }
  160.  
  161. // Crée un élément HTML avec des styles
  162. createElement(tag, styles = {}) {
  163. const element = document.createElement(tag);
  164. Object.assign(element.style, styles);
  165. return element;
  166. }
  167.  
  168. // Crée le bouton précédent ou suivant
  169. createButton(text, position) {
  170.  
  171. const isMobileDevice = isMobile();
  172.  
  173. const button = this.createElement('button', {
  174. position: 'absolute',
  175. [position]: '5px',
  176. backgroundColor: 'rgba(0, 0, 0, 0.6)',
  177. color: 'white',
  178. fontSize: isMobileDevice ? '18px' : '22px',//'22px',
  179. border: 'none',
  180. borderRadius: '50%',
  181. width: isMobileDevice ? '35px' : '40px',//'40px',
  182. height: isMobileDevice ? '35px' : '40px',//'40px',
  183. cursor: 'pointer',
  184. display: 'flex',
  185. alignItems: 'center',
  186. justifyContent: 'center',
  187. boxShadow: '0px 4px 8px rgba(0, 0, 0, 0.6)',
  188. transition: 'background-color 0.3s, transform 0.3s'
  189. });
  190.  
  191.  
  192. //button.textContent = text;*
  193. const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
  194. svg.setAttribute('viewBox', '0 0 24 24');
  195. svg.setAttribute('width', '24');
  196. svg.setAttribute('height', '24');
  197. svg.setAttribute('fill', 'white');
  198.  
  199. const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  200. path.setAttribute('d', position === 'left'
  201. ? 'M15.41 16.59L10.83 12l4.58-4.59L14 6l-6 6 6 6z' // Icône flèche gauche
  202. : 'M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6z'); // Icône flèche droite
  203. svg.appendChild(path);
  204. button.appendChild(svg);
  205.  
  206. this.addButtonEffects(button);
  207.  
  208. return button;
  209. }
  210.  
  211. createDownloadButton() {
  212. const isMobileDevice = isMobile();
  213.  
  214. const button = this.createElement('button', {
  215. position: 'relative',
  216. backgroundColor: 'rgba(0, 0, 0, 0.5)',
  217. color: 'white',
  218. fontSize: isMobileDevice ? '12px' : '10px',
  219. border: '1px solid rgba(255, 255, 255, 0.3)',
  220. borderRadius: '50%',
  221. padding: '0',
  222. cursor: 'pointer',
  223. zIndex: 10001,
  224. display: 'flex',
  225. alignItems: 'center',
  226. justifyContent: 'center',
  227. width: isMobileDevice ? '37px' : '45px',
  228. height: isMobileDevice ? '37px' : '45px',
  229. boxShadow: '0 2px 4px rgba(0, 0, 0, 0.5)',
  230. transition: 'transform 0.3s ease, background-color 0.3s ease',
  231. });
  232. button.setAttribute('title', 'Enregistrer l\'image');
  233.  
  234. const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
  235. svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
  236. svg.setAttribute('viewBox', '0 0 24 24');
  237. svg.setAttribute('width', isMobileDevice ? '18' : '22');
  238. svg.setAttribute('height', isMobileDevice ? '18' : '22');
  239. svg.setAttribute('fill', 'none');
  240. svg.setAttribute('stroke', 'currentColor');
  241. svg.setAttribute('stroke-linecap', 'round');
  242. svg.setAttribute('stroke-linejoin', 'round');
  243. svg.setAttribute('stroke-width', '2');
  244.  
  245. const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  246. path.setAttribute('d', 'M6 21h12M12 3v14m0 0l5-5m-5 5l-5-5');
  247.  
  248. svg.appendChild(path);
  249. button.appendChild(svg);
  250. this.addButtonEffects(button);
  251.  
  252. return button;
  253. }
  254.  
  255. createSearchButton() {
  256. const isMobileDevice = isMobile();
  257.  
  258. const button = this.createElement('button', {
  259. position: 'relative',
  260. backgroundColor: 'rgba(0, 0, 0, 0.6)',
  261. color: 'white',
  262. fontSize: isMobileDevice ? '12px' : '10px',
  263. border: '1px solid rgba(255, 255, 255, 0.3)',
  264. borderRadius: '50%',
  265. padding: '0',
  266. cursor: 'pointer',
  267. zIndex: 10001,
  268. display: 'flex',
  269. alignItems: 'center',
  270. justifyContent: 'center',
  271. width: isMobileDevice ? '37px' : '45px',
  272. height: isMobileDevice ? '37px' : '45px',
  273. boxShadow: '0 2px 4px rgba(0, 0, 0, 0.5)',
  274. transition: 'transform 0.3s ease, background-color 0.3s ease',
  275. });
  276. button.setAttribute('title', 'Rechercher par image');
  277.  
  278.  
  279. const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
  280. svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
  281. svg.setAttribute('width', isMobileDevice ? '18' : '22');
  282. svg.setAttribute('height', isMobileDevice ? '18' : '22');
  283. svg.setAttribute('viewBox', '0 0 24 24');
  284. svg.setAttribute('fill', 'currentColor');
  285.  
  286. const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  287. path.setAttribute('fill', 'currentColor');
  288. 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');
  289.  
  290. svg.appendChild(path);
  291. button.appendChild(svg);
  292.  
  293. button.addEventListener('click', () => this.searchImageOnGoogle());
  294.  
  295. this.addButtonEffects(button);
  296.  
  297. return button;
  298. }
  299.  
  300.  
  301. createOptionButton() {
  302. const isMobileDevice = isMobile();
  303.  
  304. const button = this.createElement('button', {
  305. position: 'relative',
  306. backgroundColor: 'rgba(0, 0, 0, 0.6)',
  307. color: 'white',
  308. fontSize: isMobileDevice ? '12px' : '10px',
  309. border: '1px solid rgba(255, 255, 255, 0.3)',
  310. borderRadius: '50%',
  311. padding: '0',
  312. cursor: 'pointer',
  313. zIndex: 10001,
  314. display: 'flex',
  315. alignItems: 'center',
  316. justifyContent: 'center',
  317. width: isMobileDevice ? '37px' : '45px',
  318. height: isMobileDevice ? '37px' : '45px',
  319. boxShadow: '0 2px 4px rgba(0, 0, 0, 0.5)',
  320. transition: 'transform 0.3s ease, background-color 0.3s ease',
  321. });
  322. button.setAttribute('title', 'Personnaliser');
  323.  
  324. // Création du SVG avec trois points alignés verticalement
  325. const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
  326. svg.setAttribute('viewBox', '0 0 24 24');
  327. svg.setAttribute('width', isMobileDevice ? '18' : '22');
  328. svg.setAttribute('height', isMobileDevice ? '18' : '22');
  329. svg.setAttribute('fill', 'currentColor');
  330.  
  331. // Création des trois cercles pour les trois points
  332. const circle1 = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
  333. circle1.setAttribute('cx', '12');
  334. circle1.setAttribute('cy', '5');
  335. circle1.setAttribute('r', '2');
  336.  
  337. const circle2 = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
  338. circle2.setAttribute('cx', '12');
  339. circle2.setAttribute('cy', '12');
  340. circle2.setAttribute('r', '2');
  341.  
  342. const circle3 = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
  343. circle3.setAttribute('cx', '12');
  344. circle3.setAttribute('cy', '19');
  345. circle3.setAttribute('r', '2');
  346.  
  347. // Ajout des cercles dans le SVG
  348. svg.appendChild(circle1);
  349. svg.appendChild(circle2);
  350. svg.appendChild(circle3);
  351.  
  352. // Ajout du SVG dans le bouton
  353. button.appendChild(svg);
  354.  
  355. // Créer le menu d'options
  356. //this.optionsMenu = this.createOptionsMenu();
  357.  
  358. this.addButtonEffects(button);
  359.  
  360. return button;
  361. }
  362.  
  363. // Crée le bouton de fermeture
  364. createCloseButton() {
  365. const isMobileDevice = isMobile();
  366.  
  367. const button = this.createElement('button', {
  368. position: 'absolute',
  369. top: '80px',
  370. right: '10px',
  371. backgroundColor: 'rgba(0, 0, 0, 0.8)',
  372. color: 'white',
  373. fontSize: isMobileDevice ? '18px' : '16px',
  374. //border: 'none',
  375. border: '1px solid rgba(255, 255, 255, 0.3)',
  376. borderRadius: '50%',
  377. width: isMobileDevice ? '40px' : '35px',
  378. height: isMobileDevice ? '40px' : '35px',
  379. cursor: 'pointer',
  380. zIndex: 99999999
  381. });
  382.  
  383. button.textContent = '✕';
  384. this.addButtonEffects(button);
  385.  
  386. return button;
  387. }
  388.  
  389. // Crée la zone d'affichage du texte d'information (numéro d'image)
  390. createInfoText() {
  391. return this.createElement('div', {
  392. position: 'absolute',
  393. //top: '80px',
  394. bottom: '0px',
  395. //left: '15px',
  396. right: '10px',
  397. color: 'white',
  398. fontSize: '12px',
  399. backgroundColor: 'rgba(5, 5, 5, 0.5)',
  400. padding: '5px',
  401. borderRadius: '5px',
  402. zIndex: 10001
  403. });
  404. }
  405.  
  406. // Crée un spinner pour indiquer le chargement de l'image
  407. createSpinner() {
  408. const spinner = this.createElement('div', {
  409. position: 'absolute',
  410. border: '8px solid #f3f3f3',
  411. borderTop: '8px solid #3498db',
  412. borderRadius: '50%',
  413. width: '50px',
  414. height: '50px',
  415. animation: 'spin 1s linear infinite',
  416. zIndex: 10001
  417. });
  418. return spinner;
  419. }
  420.  
  421. createOptionsMenu() {
  422. const optionsMenu = this.createElement('div', {
  423. position: 'relative',
  424. backgroundColor: 'rgba(5, 5, 5, 0.8)',
  425. color: 'white',
  426. padding: '10px',
  427. borderRadius: '5px',
  428. zIndex: 10001,
  429. display: 'none', // Caché par défaut
  430. flexDirection: 'column',
  431. });
  432. optionsMenu.style.position = 'absolute';
  433. optionsMenu.style.bottom = '20px';
  434. optionsMenu.style.right = '60px';
  435.  
  436. optionsMenu.appendChild(this.createCheckboxOption(
  437. 'Afficher le bouton de téléchargement',
  438. true,
  439. this.downloadButton,
  440. (checked) => {
  441. this.downloadButton.style.display = checked ? 'block' : 'none';
  442. checked ? this.downloadButton.removeAttribute('data-hidden-by-options') : this.downloadButton.setAttribute('data-hidden-by-options', 'true');
  443. }
  444. ));
  445.  
  446. optionsMenu.appendChild(this.createCheckboxOption(
  447. 'Afficher les miniatures',
  448. true,
  449. this.thumbnailPanel,
  450. (checked) => {
  451. this.thumbnailPanel.style.display = checked ? 'block' : 'none';
  452. checked ? this.thumbnailPanel.removeAttribute('data-hidden-by-options') : this.thumbnailPanel.setAttribute('data-hidden-by-options', 'true');
  453. }
  454. ));
  455.  
  456. optionsMenu.appendChild(this.createCheckboxOption(
  457. 'Afficher le bouton Google Lens',
  458. false,
  459. this.searchButton,
  460. (checked) => {
  461. this.searchButton.style.display = checked ? 'block' : 'none';
  462. checked ? this.searchButton.removeAttribute('data-hidden-by-options') : this.searchButton.setAttribute('data-hidden-by-options', 'true');
  463. }
  464. ));
  465.  
  466. return optionsMenu;
  467. }
  468.  
  469. // Fonction pour créer une option avec une case à cocher
  470. createCheckboxOption(labelText, isChecked = false, elementDepend, onChange) {
  471. const container = this.createElement('div', {
  472. display: 'flex',
  473. alignItems: 'center',
  474. margin: '5px 0',
  475. cursor: 'pointer',
  476. userSelect: 'none',
  477. });
  478.  
  479. const checkboxId = `jvcimageviwer-checkbox-${labelText.replace(/\s+/g, '-').toLowerCase()}`;
  480.  
  481. // Mettre un drapeau indiquant si l'element doit etre caché ou pas si il y a eu une réponse dans le localStorage
  482. const storedValue = localStorage.getItem(checkboxId);
  483. if (storedValue != null) {
  484. isChecked = (storedValue === "true");
  485. if (elementDepend) {
  486. isChecked ? elementDepend.removeAttribute('data-hidden-by-options') : elementDepend.setAttribute('data-hidden-by-options', 'true');
  487. elementDepend.style.display = isChecked ? 'block' : 'none';
  488. }
  489. } else if (!isChecked) {
  490. elementDepend.setAttribute('data-hidden-by-options', 'true');
  491. }
  492.  
  493. const checkbox = this.createElement('input');
  494. checkbox.setAttribute('type', 'checkbox');
  495. checkbox.checked = isChecked;
  496.  
  497. // Donne un ID unique à la case à cocher pour l'associer au label
  498. checkbox.setAttribute('id', checkboxId);
  499.  
  500. // Écouteur d'événement pour changer la valeur
  501. checkbox.addEventListener('change', (e) => {
  502. onChange(e.target.checked);
  503. localStorage.setItem(checkboxId, e.target.checked);
  504. });
  505.  
  506. const label = this.createElement('label');
  507. label.textContent = labelText;
  508. label.setAttribute('for', checkboxId);
  509. label.style.marginLeft = '10px';
  510.  
  511. container.append(checkbox, label);
  512.  
  513. // Ajout d'un écouteur d'événement sur le conteneur pour activer la case à cocher
  514. container.addEventListener('click', () => {
  515. if (event.target !== checkbox && event.target !== label) {
  516. checkbox.checked = !checkbox.checked;
  517. onChange(checkbox.checked);
  518. localStorage.setItem(checkboxId, checkbox.checked);
  519. }
  520. });
  521.  
  522. return container;
  523. }
  524.  
  525. createThumbnailPannel() {
  526. const thumbnailPanel = this.createElement('div', {
  527. position: 'fixed',
  528. bottom: '10px',
  529. left: '50%',
  530. transform: 'translateX(-50%)',
  531. border: '0px solid',
  532. padding: '0px',
  533. zIndex: '1010',
  534. maxHeight: '80px',
  535. maxWidth: '80%',
  536. overflowY: 'hidden',
  537. overflowX: 'auto',
  538. display: 'flex',
  539. alignItems: 'center',
  540. backgroundColor: 'transparent',
  541. });
  542. thumbnailPanel.classList.add('thumbnail-scroll-container');
  543.  
  544. return thumbnailPanel;
  545. }
  546.  
  547. addButtonEffects(button) {
  548. button.addEventListener('mouseenter', () => {
  549. button.style.backgroundColor = 'rgba(255, 255, 255, 0.8)';
  550. button.style.color = 'black';
  551. button.style.transform = 'scale(1.1)';
  552. });
  553.  
  554. button.addEventListener('mouseleave', () => {
  555. button.style.backgroundColor = 'rgba(0, 0, 0, 0.6)';
  556. button.style.color = 'white';
  557. button.style.transform = 'scale(1)';
  558. });
  559.  
  560. button.addEventListener('mousedown', () => {
  561. button.style.transform = 'scale(0.9)';
  562. });
  563.  
  564. button.addEventListener('mouseup', () => {
  565. button.style.transform = 'scale(1.1)';
  566. });
  567. }
  568.  
  569. toggleMenuOptions() {
  570. if (this.optionsMenu.style.display === 'none') {
  571. this.optionsMenu.style.display = 'flex';
  572. } else {
  573. this.optionsMenu.style.display = 'none';
  574. }
  575. }
  576.  
  577. // Ajoute les événements aux différents éléments du visualiseur
  578. addEventListeners() {
  579. // Bouttons de controles du visualiseur
  580. this.prevButton.addEventListener('click', () => this.changeImage(-1));
  581. this.nextButton.addEventListener('click', () => this.changeImage(1));
  582. this.closeButton.addEventListener('click', () => this.closeViewer());
  583. this.downloadButton.addEventListener('click', () => this.startDownload());
  584. this.optionButton.addEventListener('click', () => this.toggleMenuOptions());
  585. this.overlay.addEventListener('click', (event) => {
  586. if (event.target === this.overlay) {
  587. this.closeViewer();
  588. }
  589. });
  590.  
  591. // Zoom avec la molette de la souris
  592. this.imgElement.addEventListener('wheel', (event) => this.handleZoom(event));
  593.  
  594.  
  595. // Déplacement lors du zoom (drag)
  596. this.imgElement.addEventListener('mousedown', (event) => this.startDrag(event));
  597. this.imgElement.addEventListener('mousedown', this.handleMouseDown.bind(this));
  598. this.imgElement.addEventListener('mouseup', this.handleMouseUp.bind(this));
  599.  
  600. // Touches avec les doigts
  601. this.imgElement.addEventListener('touchstart', (event) => this.handleTouchEvent(event));
  602. this.imgElement.addEventListener('touchmove', (event) => this.handleTouchEvent(event));
  603. this.imgElement.addEventListener('touchend', (event) => this.handleTouchEvent(event));
  604.  
  605. // Ouvrir l'image dans une no6velle fenêtre
  606. this.imgElement.addEventListener('click', () => {
  607. if (!this.isDragging) {
  608. window.open(this.images[this.currentIndex].href, '_blank');
  609. }
  610. });
  611.  
  612. // Touches du clavier
  613. document.addEventListener('keydown', (event) => this.handleKeyboardEvents(event));
  614. }
  615.  
  616. // Fonctions pour gérer les touches du clavier
  617. handleKeyboardEvents(event) {
  618. switch (event.key) {
  619. case 'ArrowLeft':
  620. case 'ArrowUp':
  621. this.changeImage(-1);
  622. break;
  623. case 'ArrowRight':
  624. case 'ArrowDown':
  625. this.changeImage(1);
  626. break;
  627. case 'Escape':
  628. event.preventDefault();
  629. this.closeViewer();
  630. break;
  631. }
  632. }
  633.  
  634. // Fonctions pour gérer les touches tactiles
  635. handleTouchEvent(event) {
  636. switch (event.type) {
  637. case 'touchstart':
  638. if (event.touches.length === 1) {
  639. if (this.imageElementScale > 1) {
  640. // Si l'image est zoomée, permettre le déplacement (drag)
  641. this.startDrag(event);
  642. } else {
  643. // Sinon, démarrer le swipe
  644. this.handleSwipeStart(event);
  645. }
  646. } else if (event.touches.length === 2) {
  647. // Démarrer le pinch zoom
  648. this.handlePinchStart(event);
  649. }
  650. break;
  651.  
  652. case 'touchmove':
  653. if (event.touches.length === 1) {
  654. if (this.imageElementScale > 1) {
  655. // Si l'image est zoomée, permettre le déplacement (drag)
  656. this.onDrag(event);
  657. } else {
  658. this.handleSwipeMove(event);
  659. }
  660. } else if (event.touches.length === 2) {
  661. // Gérer le pinch zoom
  662. this.handlePinchMove(event);
  663. }
  664. break;
  665.  
  666. case 'touchend':
  667. if (event.touches.length === 1) {
  668. if (this.imageElementScale > 1) {
  669. this.endDrag(event);
  670. }
  671. }
  672. else if (event.touches.length === 0) {
  673. if (this.isSwiping) {
  674. this.handleSwipeEnd(event);
  675. } else if (this.isPinchZooming) {
  676. this.handlePinchEnd(event);
  677. } else {
  678. this.endDrag(event);
  679. }
  680. }
  681. break;
  682. }
  683. }
  684.  
  685.  
  686. // Gestion du début de l'interaction tactile
  687. handleSwipeStart(event) {
  688. if (event.touches.length === 1) {
  689. if(this.isPinchZooming) {
  690. return;
  691. }
  692. // Commencer le swipe
  693. this.isSwiping = true;
  694. this.startX = event.touches[0].clientX;
  695. this.startY = event.touches[0].clientY;
  696. }
  697. }
  698.  
  699. // Gestion du mouvement tactile pour swipe
  700. handleSwipeMove(event) {
  701. if (this.isSwiping && event.touches.length === 1) {
  702. this.currentX = event.touches[0].clientX;
  703. this.currentY = event.touches[0].clientY;
  704. }
  705. }
  706.  
  707. // Gestion de la fin de l'interaction tactile
  708. handleSwipeEnd(event) {
  709. if (event.touches.length === 0) {
  710. this.initialDistance = null;
  711. }
  712. if (this.isSwiping) {
  713. this.isSwiping = false;
  714. const deltaX = this.currentX - this.startX;
  715. const deltaY = this.currentY - this.startY;
  716.  
  717. // Si le mouvement est suffisamment grand, on change d'image
  718. if (Math.abs(deltaX) > 50) {
  719. if (deltaX > 0) {
  720. this.showPreviousImage();
  721. } else {
  722. this.showNextImage();
  723. }
  724. }
  725.  
  726. // Si le mouvement est suffisamment grand verticalement, on ferme le visualiseur
  727. if (Math.abs(deltaY) > 50) {
  728. this.closeViewer();
  729. }
  730. }
  731.  
  732. // l'image revient à sa taille d'origine, réinitialiser le zIndex
  733. this.imgElement.style.zIndex = '';
  734. }
  735.  
  736. // Calculate distance between two fingers
  737. distance(event){
  738. return Math.hypot(event.touches[0].pageX - event.touches[1].pageX, event.touches[0].pageY - event.touches[1].pageY);
  739. }
  740.  
  741. // Gestion du début de l'interaction tactile pour le pincement
  742. handlePinchStart(event) {
  743. if (event.touches.length === 2) {
  744. event.preventDefault(); // Prevent page scroll
  745. this.isPinchZooming = true;
  746.  
  747. // Calculate where the fingers have started on the X and Y axis
  748. this.start.x = (event.touches[0].pageX + event.touches[1].pageX) / 2;
  749. this.start.y = (event.touches[0].pageY + event.touches[1].pageY) / 2;
  750. this.start.distance = this.distance(event);
  751.  
  752. // Save the current scale to use it as a base for the new scale
  753. this.initialScale = this.imageElementScale || 1; // Use 1 if there's no previous zoom
  754. }
  755. }
  756.  
  757. // Gestion du mouvement tactile pour le pincement (zoom)
  758. handlePinchMove(event) {
  759. if (event.touches.length === 2) {
  760. event.preventDefault(); // Prevent page scroll
  761.  
  762. // Safari provides event.scale as two fingers move on the screen
  763. // For other browsers just calculate the scale manually
  764. let scale;
  765. if (event.scale) {
  766. scale = event.scale;
  767. } else {
  768. const deltaDistance = this.distance(event);
  769. scale = (deltaDistance / this.start.distance); //* this.touchSensitivityFactor;
  770. }
  771. // this.imageElementScale = Math.min(Math.max(1, scale), 4);
  772. // Multiply the new scale by the starting scale to retain the zoom level
  773. // this.imageElementScale = Math.min(Math.max(1, this.startScale * scale), 4);
  774. this.imageElementScale = Math.min(Math.max(1, this.initialScale * scale), 4);
  775.  
  776.  
  777. // Calculate how much the fingers have moved on the X and Y axis
  778. const deltaX = (((event.touches[0].pageX + event.touches[1].pageX) / 2) - this.start.x) * 2; // x2 for accelarated movement
  779. const deltaY = (((event.touches[0].pageY + event.touches[1].pageY) / 2) - this.start.y) * 2; // x2 for accelarated movement
  780.  
  781. // Transform the image to make it grow and move with fingers
  782. const transform = `translate3d(${deltaX}px, ${deltaY}px, 0) scale(${this.imageElementScale})`;
  783. this.imgElement.style.transform = transform;
  784. this.imgElement.style.WebkitTransform = transform;
  785. this.imgElement.style.zIndex = "9999";
  786. this.closeButton.style.zIndex = "10003";
  787. }
  788. }
  789.  
  790. // Gestion de la fin de l'interaction tactile pour le pincement
  791. handlePinchEnd(event) {
  792. if (event.touches.length < 2) {
  793. // Ajouter un délai pour réinitialiser le drag (empeche les conflits avec le clic)
  794. this.dragTimeout = setTimeout(() => {
  795. this.isPinchZooming = false;
  796. this.dragTimeout = null;
  797. }, 100);
  798.  
  799. // Si l'image est revenue à sa taille d'origine, réinitialiser le zIndex
  800. if (this.imageElementScale <= 1) {
  801. this.imgElement.style.zIndex = '';
  802. }
  803. }
  804. }
  805.  
  806. // Fonction pour calculer la distance entre deux points de contact
  807. getTouchDistance(touches) {
  808. const [touch1, touch2] = touches;
  809. const deltaX = touch2.clientX - touch1.clientX;
  810. const deltaY = touch2.clientY - touch1.clientY;
  811. return Math.sqrt(deltaX * deltaX + deltaY * deltaY);
  812. }
  813.  
  814.  
  815. handleZoom(event) {
  816. event.preventDefault();
  817. const zoomIncrement = 0.07; // Sensibilité du zoom
  818.  
  819. let previousZoomLevel = this.imageElementScale;
  820.  
  821. // Calcul du nouveau niveau de zoom
  822. if (event.deltaY < 0) {
  823. this.imageElementScale = Math.min(4, this.imageElementScale + zoomIncrement); // Zoom max
  824. } else {
  825. this.imageElementScale = Math.max(1, this.imageElementScale - zoomIncrement); // Zoom min
  826. }
  827.  
  828. // Calcul de la position relative du zoom pour recentrer autour du point de la molette
  829. let imgRect = this.imgElement.getBoundingClientRect();
  830. let offsetXRelative = (event.clientX - this.offsetX) / previousZoomLevel;
  831. let offsetYRelative = (event.clientY - this.offsetY) / previousZoomLevel;
  832.  
  833. // Recalculer les offsets pour centrer l'image correctement après le zoom
  834. this.offsetX = event.clientX - offsetXRelative * this.imageElementScale;
  835. this.offsetY = event.clientY - offsetYRelative * this.imageElementScale;
  836.  
  837. // Appliquer les limites de déplacement après zoom
  838. this.limitOffsets();
  839.  
  840. // Appliquer la transformation avec le zoom et les offsets
  841. //this.imgElement.style.transform = `translate(${this.offsetX}px, ${this.offsetY}px) scale(${this.imageElementScale})`;
  842.  
  843. // Ajuster le z-index si zoomé
  844. this.applyTransform();
  845. }
  846.  
  847. applyTransform() {
  848. this.imgElement.style.transition = 'transform 0.1s ease'; // Ajout d'une transition fluide
  849. this.imgElement.style.transform = `translate(${this.offsetX}px, ${this.offsetY}px) scale(${this.imageElementScale})`;
  850.  
  851. // Ajuster le z-index si zoomé
  852. this.imgElement.style.zIndex = this.imageElementScale > 1 ? 10002 : '';
  853. }
  854.  
  855. limitOffsets() {
  856. let imgRect = this.imgElement.getBoundingClientRect();
  857. let windowWidth = window.innerWidth;
  858. let windowHeight = window.innerHeight;
  859.  
  860. // Calculer les limites maximales
  861. let maxOffsetX = Math.max(0, (imgRect.width * this.imageElementScale) - windowWidth);
  862. let maxOffsetY = Math.max(0, (imgRect.height * this.imageElementScale) - windowHeight);
  863.  
  864. // Limiter les déplacements
  865. this.offsetX = Math.min(Math.max(this.offsetX, -maxOffsetX), 0);
  866. this.offsetY = Math.min(Math.max(this.offsetY, -maxOffsetY), 0);
  867. }
  868.  
  869. startDrag(event) {
  870. event.preventDefault(); // Empêche la sélection de l'image
  871.  
  872. // Gestion tactile
  873. if (event.type === 'touchstart') {
  874. this.isTouchDragging = true;
  875. this.startX = event.touches[0].clientX;
  876. this.startY = event.touches[0].clientY;
  877. } else {
  878. // Gestion avec la souris
  879. this.isMouseDown = true;
  880. this.startX = event.clientX;
  881. this.startY = event.clientY;
  882. }
  883.  
  884. this.isDragging = true;
  885. this.imgElement.style.cursor = 'grabbing';
  886. this.imgElement.style.userSelect = 'none';
  887.  
  888. // Ajouter des listeners sur le document pour capturer les mouvements
  889. if (event.touches) {
  890. document.addEventListener('touchmove', this.onDragBound = this.onDrag.bind(this));
  891. document.addEventListener('touchend', this.endDragBound = this.endDrag.bind(this));
  892. } else {
  893. document.addEventListener('mousemove', this.onDragBound = this.onDrag.bind(this));
  894. document.addEventListener('mouseup', this.endDragBound = this.endDrag.bind(this));
  895. }
  896. }
  897.  
  898. onDrag(event) {
  899. if (!this.isDragging) return;
  900.  
  901. event.preventDefault();
  902.  
  903. let deltaX, deltaY;
  904. if (event.type === 'touchmove') {
  905. // Gestion tactile
  906. deltaX = (event.touches[0].clientX - this.startX) * this.touchSensitivityFactor;
  907. deltaY = (event.touches[0].clientY - this.startY) * this.touchSensitivityFactor;
  908. } else {
  909. // Gestion avec la souris
  910. deltaX = (event.clientX - this.startX) * this.mouseSensitivityFactor;
  911. deltaY = (event.clientY - this.startY) * this.mouseSensitivityFactor;
  912. }
  913.  
  914. // Calculer les nouveaux offsets
  915. let newOffsetX = this.offsetX + deltaX;
  916. let newOffsetY = this.offsetY + deltaY;
  917.  
  918. // Limiter les nouveaux offsets
  919. this.offsetX = Math.min(Math.max(newOffsetX, -this.imgElement.width * (this.imageElementScale - 1)), 0);
  920. this.offsetY = Math.min(Math.max(newOffsetY, -this.imgElement.height * (this.imageElementScale - 1)), 0);
  921.  
  922. // Appliquer la translation ajustée par le facteur de sensibilité
  923. this.offsetX += deltaX;
  924. this.offsetY += deltaY;
  925.  
  926. // Appliquer la transformation avec le zoom et la translation
  927. this.applyTransform();
  928.  
  929. // Mettre à jour les points de départ pour le prochain déplacement
  930. this.startX = event.type === 'touchmove' ? event.touches[0].clientX : event.clientX;
  931. this.startY = event.type === 'touchmove' ? event.touches[0].clientY : event.clientY;
  932. }
  933.  
  934. endDrag(event) {
  935. this.imgElement.style.cursor = 'grab';
  936.  
  937. // Retirer les listeners
  938. if (event.type === 'touchend') {
  939. this.isTouchDragging = false; // Réinitialise l'état tactile
  940. document.removeEventListener('touchmove', this.onDragBound);
  941. document.removeEventListener('touchend', this.endDragBound);
  942. } else {
  943. this.isMouseDown = false; // Réinitialise l'état de la souris
  944. document.removeEventListener('mousemove', this.onDragBound);
  945. document.removeEventListener('mouseup', this.endDragBound);
  946. }
  947.  
  948. // Réinitialiser le drag après un délai pour éviter les conflits
  949. this.dragTimeout = setTimeout(() => {
  950. this.isDragging = false;
  951. this.imgElement.style.cursor = 'pointer';
  952. this.dragTimeout = null;
  953. }, 100);
  954. }
  955.  
  956.  
  957. handleMouseDown(event) {
  958. this.isMouseDown = true;
  959. this.mouseDownX = event.clientX;
  960. this.mouseDownY = event.clientY;
  961.  
  962. // Démarrer le drag après un délai pour éviter le drag lors des clics courts
  963. this.dragTimeout = setTimeout(() => {
  964. if (this.isMouseDown) {
  965. this.startDrag(event);
  966. }
  967. }, 200);
  968. }
  969.  
  970. handleMouseUp(event) {
  971. this.isMouseDown = false;
  972.  
  973. // Si le délai pour démarrer le drag est encore en cours, le nettoyer
  974. if (this.dragTimeout) {
  975. clearTimeout(this.dragTimeout);
  976. this.dragTimeout = null;
  977. }
  978.  
  979. // Vérifier si le mouvement est suffisant pour considérer que c'est un drag
  980. const movedX = Math.abs(event.clientX - this.mouseDownX);
  981. const movedY = Math.abs(event.clientY - this.mouseDownY);
  982.  
  983. if (movedX < 5 && movedY < 5) {
  984. // handleImageClick(event); // Traiter le clic si le mouvement est minime
  985. }
  986. }
  987.  
  988. // Réinitialiser le zoom de l'image
  989. resetZoom() {
  990. this.imgElement.style.transform = 'scale(1)';
  991. this.imgElement.style.transformOrigin = '0 0';
  992. this.imageElementScale = 1;
  993. this.offsetX = 0;
  994. this.offsetY = 0;
  995. }
  996.  
  997. // Réinitialiser la position du drag de l'image
  998. resetDrag() {
  999. this.imgElement.style.left = '0px';
  1000. this.imgElement.style.top = '0px';
  1001. }
  1002.  
  1003.  
  1004. // Met à jour l'image affichée dans le visualiseur
  1005. updateImage() {
  1006. if (this.currentIndex >= 0 && this.currentIndex < this.images.length) {
  1007. const imageUrl = this.images[this.currentIndex].href;
  1008.  
  1009. this.imgElement.src = imageUrl;
  1010. this.infoText.textContent = `${this.currentIndex + 1} / ${this.images.length}`;
  1011. this.spinner.style.display = 'block';
  1012.  
  1013. this.toggleButtonState();
  1014.  
  1015. this.imgElement.onload = () => {
  1016. this.imgElement.style.opacity = 1;
  1017. this.spinner.style.display = 'none';
  1018.  
  1019. // Réinitialiser le zoom et la position du drag
  1020. this.resetZoom();
  1021. this.resetDrag();
  1022.  
  1023. // Calcul de la position des boutons
  1024. const imgRect = this.imgElement.getBoundingClientRect();
  1025. const isMobileDevice = isMobile(); // Détection des mobiles
  1026.  
  1027. if (imgRect.width > this.defaultImageWidth) {
  1028. this.defaultImageWidth = imgRect.width;
  1029. }
  1030.  
  1031. if (isMobileDevice) {
  1032. // pass
  1033. } else {
  1034. const margin = 30;
  1035. // Calcul de la position des boutons
  1036. let prevButtonLeft = (window.innerWidth - this.defaultImageWidth) / 2 - this.prevButton.offsetWidth - margin;
  1037. let nextButtonRight = (window.innerWidth - this.defaultImageWidth) / 2 - this.nextButton.offsetWidth - margin;
  1038.  
  1039. // Limite les boutons pour qu'ils ne sortent pas de l'écran à gauche ou à droite
  1040. prevButtonLeft = Math.max(prevButtonLeft, margin); // Empêche de sortir à gauche
  1041. nextButtonRight = Math.max(nextButtonRight, margin); // Empêche de sortir à droite
  1042.  
  1043. // Appliquer les positions ajustées
  1044. this.prevButton.style.left = `${prevButtonLeft}px`;
  1045. this.nextButton.style.right = `${nextButtonRight}px`;
  1046. }
  1047.  
  1048. this.focusOnThumbnail();
  1049. };
  1050.  
  1051. this.imgElement.onerror = () => this.handleImageError();
  1052. }
  1053. }
  1054.  
  1055. // Gestion des erreurs de chargement d'image
  1056. handleImageError() {
  1057. const miniUrl = this.images[this.currentIndex].querySelector('img').src;
  1058. const fullUrl = this.images[this.currentIndex].href;
  1059. const extensions = this.reorderExtensions(fullUrl);
  1060. const baseUrl = miniUrl.replace('/minis/', '/fichiers/');
  1061.  
  1062. const tryNextExtension = (index) => {
  1063. if (index >= extensions.length) {
  1064. // Si toutes les extensions échouent, tenter l'URL originale (mini)
  1065. this.imgElement.src = miniUrl;
  1066. const imgTestMini = new Image();
  1067. imgTestMini.src = miniUrl;
  1068.  
  1069. imgTestMini.onload = () => {
  1070. this.imgElement.src = miniUrl;
  1071. };
  1072. imgTestMini.onerror = () => {
  1073. this.setImageNotFound(this.imgElement);
  1074. };
  1075. return;
  1076. }
  1077.  
  1078. // Remplacer l'extension et mettre à jour l'URL
  1079. const updatedUrl = baseUrl.replace(/\.(jpg|png|jpeg|webp|gif)$/, extensions[index]);
  1080.  
  1081. // Tester l'URL avec un élément Image temporaire
  1082. const imgTest = new Image();
  1083. imgTest.src = updatedUrl;
  1084.  
  1085. imgTest.onload = () => {
  1086. this.imgElement.src = updatedUrl;
  1087. };
  1088.  
  1089. imgTest.onerror = () => {
  1090. // console.log("Error loading: " + updatedUrl);
  1091. tryNextExtension(index + 1);
  1092. };
  1093. };
  1094.  
  1095. // Commencer les essais avec la première extension
  1096. tryNextExtension(0);
  1097. }
  1098.  
  1099. setImageNotFound(imageElement) {
  1100. const notFoundImageUrl = "https://upload.wikimedia.org/wikipedia/commons/a/ac/No_image_available.svg";
  1101. imageElement.src = notFoundImageUrl;
  1102. }
  1103.  
  1104. // Réarranger la liste des extensions a tester pour mettre l'extension utilisée sur noelshack en premier
  1105. reorderExtensions(currentImageUrl) {
  1106. const extensions = ['.jpg', '.png', '.jpeg'];
  1107. const currentExtension = getImageExtension(currentImageUrl);
  1108. const newExtensions = [...extensions];
  1109.  
  1110. if (currentExtension) {
  1111. if (!newExtensions.includes(currentExtension)) {
  1112. newExtensions.unshift(currentExtension);
  1113. } else {
  1114. const index = newExtensions.indexOf(currentExtension);
  1115. if (index > -1) {
  1116. newExtensions.splice(index, 1);
  1117. newExtensions.unshift(currentExtension);
  1118. }
  1119. }
  1120. }
  1121. return newExtensions;
  1122. }
  1123.  
  1124. // Change d'image en fonction de la direction (suivant/précédent)
  1125. changeImage(direction) {
  1126. this.currentIndex = (this.currentIndex + direction + this.images.length) % this.images.length;
  1127. this.imgElement.style.opacity = 0;
  1128. this.spinner.style.display = 'block';
  1129. this.updateImage();
  1130. }
  1131.  
  1132. showPreviousImage() {
  1133. this.changeImage(-1);
  1134. }
  1135.  
  1136. showNextImage() {
  1137. this.changeImage(1);
  1138. }
  1139.  
  1140. // Désactive ou active les boutons suivant/précédent en fonction de l'index actuel
  1141. toggleButtonState() {
  1142. if (this.currentIndex === 0) {
  1143. // this.prevButton.disabled = true;
  1144. this.prevButton.style.opacity = 0.5;
  1145. this.prevButton.style.cursor = 'initial';
  1146. } else {
  1147. // this.prevButton.disabled = false;
  1148. this.prevButton.style.opacity = 1;
  1149. this.prevButton.style.cursor = 'pointer';
  1150. }
  1151.  
  1152. if (this.currentIndex === this.images.length - 1) {
  1153. // this.nextButton.disabled = true;
  1154. this.nextButton.style.opacity = 0.5;
  1155. this.nextButton.style.cursor = 'initial';
  1156. } else {
  1157. // this.nextButton.disabled = false;
  1158. this.nextButton.style.opacity = 1;
  1159. this.nextButton.style.cursor = 'pointer';
  1160. }
  1161. }
  1162.  
  1163. // Cacher temporairement le menu JVC
  1164. toggleMenuVisibility(isVisible) {
  1165. const menu = document.querySelector('.header__bottom');
  1166. if (menu) {
  1167. menu.style.display = isVisible ? '' : 'none';
  1168. }
  1169. }
  1170.  
  1171. focusOnThumbnail() {
  1172. const thumbnails = this.thumbnailPanel ? this.thumbnailPanel.querySelectorAll('img') : [];
  1173. const currentThumbnail = thumbnails[this.currentIndex];
  1174.  
  1175. // Réinitialiser les styles de la miniature précédente
  1176. if (this.previousThumbnail) {
  1177. this.previousThumbnail.style.border = 'none';
  1178. this.previousThumbnail.style.transform = 'scale(1)';
  1179. this.previousThumbnail.style.boxShadow = 'none';
  1180. this.previousThumbnail.style.filter = 'none';
  1181. this.previousThumbnail.style.zIndex = '1'; // Remettre le z-index à 1
  1182. this.previousThumbnail.style.position = 'relative';
  1183. this.previousThumbnail.style.borderRadius = '0px';
  1184. }
  1185.  
  1186. // Ajouter des effets modernes à la miniature actuelle
  1187. if (currentThumbnail) {
  1188. currentThumbnail.style.transition = 'transform 0.4s ease, box-shadow 0.4s ease, filter 0.4s ease';
  1189. currentThumbnail.style.transform = 'scale(1.3)';
  1190. currentThumbnail.style.boxShadow = '0 8px 15px rgba(0, 0, 0, 0.1), 0 0 20px rgba(254, 254, 254, 0.4)';
  1191. currentThumbnail.style.filter = 'brightness(1.2) saturate(1.3)';
  1192. currentThumbnail.style.zIndex = '10'; // Z-index élevé pour passer devant les autres
  1193. currentThumbnail.style.position = 'relative';
  1194. currentThumbnail.style.borderRadius = '2px';
  1195. currentThumbnail.parentElement.scrollIntoView({ behavior: 'smooth', inline: 'center' });
  1196. }
  1197.  
  1198. // Mettre à jour la référence de la miniature précédente
  1199. this.previousThumbnail = currentThumbnail;
  1200. }
  1201.  
  1202.  
  1203. // Fonction pour créer et afficher le panneau des miniatures
  1204. toggleThumbnailPanel() {
  1205. /*if (this.thumbnailPanel) {
  1206. this.closeThumbnailPanel(); // Ferme le panneau si déjà ouvert
  1207. return;
  1208. }*/
  1209.  
  1210. // Créer le panneau
  1211. if(this.thumbnailPanel == null) {
  1212. this.thumbnailPanel = this.createThumbnailPannel();
  1213. }
  1214.  
  1215. // Conteneur pour le défilement horizontal
  1216. const scrollContainer = this.createElement('div', {
  1217. display: 'flex',
  1218. overflowX: 'auto',
  1219. whiteSpace: 'nowrap',
  1220. maxWidth: '100%',
  1221. });
  1222.  
  1223. scrollContainer.classList.add('thumbnail-scroll-container');
  1224.  
  1225.  
  1226. // Ajout des images au conteneur
  1227. this.images.forEach((image, index) => {
  1228. const imgContainer = this.createElement('div', {
  1229. display: 'inline-block',
  1230. width: '50px',
  1231. height: '50px',
  1232. margin: '5px 2px',
  1233. padding: '4px 0px',
  1234. transition: 'transform 0.3s',
  1235. });
  1236.  
  1237. const imgElement = this.createElement('img');
  1238. imgElement.src = image.querySelector('img') ? image.querySelector('img').src : image.href || image.thumbnail;
  1239. imgElement.onerror = () => {
  1240. this.setImageNotFound(imgElement);
  1241. };
  1242.  
  1243. imgElement.alt = `Image ${index + 1}`;
  1244. imgElement.style.width = '50px';
  1245. imgElement.style.height = '100%';
  1246. imgElement.style.objectFit = 'cover';
  1247. imgElement.style.cursor = 'pointer';
  1248.  
  1249. imgElement.addEventListener('click', () => {
  1250. this.images.forEach((_, i) => {
  1251. const container = scrollContainer.children[i];
  1252. container.querySelector('img').style.border = 'none';
  1253. });
  1254. //imgElement.style.border = '2px solid blue';
  1255. this.currentIndex = index;
  1256. this.updateImage();
  1257. //imgContainer.scrollIntoView({ behavior: 'smooth', inline: 'center' });
  1258. });
  1259.  
  1260. imgContainer.appendChild(imgElement);
  1261. scrollContainer.appendChild(imgContainer);
  1262. });
  1263.  
  1264. this.thumbnailPanel.appendChild(scrollContainer);
  1265. this.overlay.appendChild(this.thumbnailPanel);
  1266.  
  1267. this.focusOnThumbnail();
  1268. }
  1269.  
  1270. // Ecouteurs d'événements pour réinitialiser le timer
  1271. addInteractionListeners() {
  1272. this.overlay.addEventListener('mousemove', this.resetHideButtons.bind(this));
  1273. this.overlay.addEventListener('click', this.resetHideButtons.bind(this));
  1274. this.overlay.addEventListener('touchstart', this.resetHideButtons.bind(this));
  1275. }
  1276.  
  1277. // Réinitialisez le timer pour cacher les boutons
  1278. resetHideButtons() {
  1279. if (this.hideButtonsTimeout) {
  1280. clearTimeout(this.hideButtonsTimeout);
  1281. }
  1282. this.toggleButtonsVisibility(true);
  1283. this.hideButtonsTimeout = setTimeout(() => {
  1284. this.toggleButtonsVisibility(false); // Cachez les boutons après 3 secondes
  1285. }, 2500);
  1286. }
  1287.  
  1288. // Changez la visibilité des boutons
  1289. toggleButtonsVisibility(visible) {
  1290. const displayValue = visible ? 'flex' : 'none';
  1291.  
  1292. const elements = [
  1293. this.prevButton,
  1294. this.nextButton,
  1295. this.thumbnailPanel,
  1296. this.infoText,
  1297. this.downloadButton,
  1298. this.searchButton,
  1299. this.optionButton,
  1300. ];
  1301.  
  1302. elements.forEach(element => {
  1303. // Vérifiez si l'élément a été masqué par le système d'options
  1304. if (element && element.hasAttribute('data-hidden-by-options')) {
  1305. // Si l'élément a été masqué par les options, ne pas le réafficher
  1306. element.style.display = 'none';
  1307. } else if (element){
  1308. element.style.display = displayValue;
  1309. }
  1310. });
  1311.  
  1312. /*if(!visible) {
  1313. this.optionsMenu.style.display = displayValue;
  1314. }*/
  1315. }
  1316.  
  1317. startDownload() {
  1318. this.downloadButton.classList.add('downloading'); // Ajout de la classe pour l'animation
  1319.  
  1320. this.downloadCurrentImage().then(() => {
  1321. // Retirer la classe après le téléchargement
  1322. this.downloadButton.classList.remove('downloading');
  1323. }).catch((error) => {
  1324. console.error('Download failed:', error);
  1325. this.downloadButton.classList.remove('downloading');
  1326. });
  1327. }
  1328.  
  1329. downloadCurrentImage() {
  1330. return new Promise((resolve, reject) => {
  1331. const imageElement = this.imgElement;
  1332. if (!imageElement) {
  1333. console.error('Image not found!');
  1334. reject('Image not found');
  1335. return;
  1336. }
  1337.  
  1338. const imageUrl = imageElement.src;
  1339. const fileNameWithExtension = imageUrl.split('/').pop();
  1340. const fileName = fileNameWithExtension.substring(0, fileNameWithExtension.lastIndexOf('.'));
  1341.  
  1342. // Utilisation de GM.xmlHttpRequest pour contourner CORS
  1343. GM.xmlHttpRequest({
  1344. method: "GET",
  1345. url: imageUrl,
  1346. responseType: "blob",
  1347. headers: {
  1348. 'Accept': 'image/jpeg,image/png,image/gif,image/bmp,image/tiff,image/*;q=0.8'
  1349. },
  1350. onload: function(response) {
  1351. if (response.status === 200) {
  1352. const blob = response.response;
  1353. const url = URL.createObjectURL(blob);
  1354.  
  1355. // Téléchargement du fichier
  1356. const a = document.createElement('a');
  1357. a.href = url;
  1358. a.download = fileName;
  1359. document.body.appendChild(a);
  1360. a.click();
  1361. document.body.removeChild(a);
  1362. URL.revokeObjectURL(url);
  1363.  
  1364. resolve(); // Indique que le téléchargement est terminé
  1365. } else {
  1366. reject('Error downloading image: ' + response.statusText);
  1367. }
  1368. },
  1369. onerror: function(err) {
  1370. reject('Request failed: ' + err);
  1371. }
  1372. });
  1373. });
  1374. }
  1375.  
  1376. searchImageOnGoogle() {
  1377. if (this.images.length > 0) {
  1378. const imageUrl = this.imgElement.src;
  1379. const googleImageSearchUrl = `https://lens.google.com/uploadbyurl?url=${encodeURIComponent(imageUrl)}`;
  1380. // Ouvrir le lien dans un nouvel onglet
  1381. window.open(googleImageSearchUrl, '_blank');
  1382. } else {
  1383. console.error('Aucune image disponible pour la recherche.');
  1384. }
  1385. }
  1386.  
  1387. // Fonction pour fermer le panneau des miniatures
  1388. closeThumbnailPanel(thumbnailPanel) {
  1389. if (this.thumbnailPanel && this.overlay.contains(this.thumbnailPanel)) {
  1390. this.overlay.removeChild(this.thumbnailPanel);
  1391. this.thumbnailPanel = null;
  1392. }
  1393. }
  1394.  
  1395. closeViewer() {
  1396. if (this.overlay) {
  1397. this.handleCloseViewer(); // Ferme le visualiseur
  1398. history.back(); // Supprime l'état ajouté par pushState
  1399. }
  1400. }
  1401.  
  1402.  
  1403. // Ferme le visualiseur d'images
  1404. handleCloseViewer() {
  1405. if (this.overlay) {
  1406. document.body.removeChild(this.overlay);
  1407.  
  1408. // Ferme le panneau des miniatures si ouvert
  1409. if (this.thumbnailPanel) {
  1410. this.closeThumbnailPanel(this.thumbnailPanel);
  1411. }
  1412.  
  1413. window.removeEventListener('popstate', this.handlePopState);
  1414.  
  1415. this.overlay = null;
  1416. this.isViewerOpen = false;
  1417. ImageViewer.instance = null; // Réinitialise l'instance singleton
  1418.  
  1419. this.toggleMenuVisibility(true);
  1420. }
  1421. }
  1422.  
  1423. openViewer(images, currentIndex) {
  1424. if (this.overlay) {
  1425. this.images = images;
  1426. this.currentIndex = currentIndex;
  1427. this.updateImage();
  1428. this.toggleThumbnailPanel();
  1429. } else {
  1430. new ImageViewer();
  1431. this.images = images;
  1432. this.currentIndex = currentIndex;
  1433. this.createOverlay();
  1434. this.updateImage();
  1435. this.toggleThumbnailPanel();
  1436. }
  1437. this.isViewerOpen = true;
  1438.  
  1439. this.addHistoryState()
  1440. window.addEventListener('popstate', this.handlePopState); // Ecouter l'événement bouton back du navigateur
  1441.  
  1442. this.toggleMenuVisibility(false);
  1443. }
  1444.  
  1445. handlePopState(event) {
  1446. if (ImageViewer.instance) {
  1447. event.preventDefault();
  1448. this.handleCloseViewer();
  1449. }
  1450. }
  1451.  
  1452. // Ajouter une entrée dans l'historique
  1453. addHistoryState() {
  1454. history.pushState({ viewerOpen: true }, '');
  1455. }
  1456. }
  1457.  
  1458. function addSpinnerStyles() {
  1459. const style = document.createElement('style');
  1460. style.textContent = `
  1461. @keyframes spin {
  1462. 0% { transform: rotate(0deg); }
  1463. 100% { transform: rotate(360deg); }
  1464. }
  1465. .spinner {
  1466. width: 50px;
  1467. height: 50px;
  1468. border-radius: 50%;
  1469. border: 5px solid transparent;
  1470. border-top: 5px solid rgba(0, 0, 0, 0.1);
  1471. background: conic-gradient(from 0deg, rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0));
  1472. animation: spin 1s linear infinite;
  1473. }
  1474. `;
  1475. document.head.appendChild(style);
  1476. }
  1477.  
  1478. function addDownloadButtonStyles() {
  1479. const style = document.createElement('style');
  1480. style.textContent = `
  1481. /* Animation de rotation */
  1482. @keyframes rotate {
  1483. 0% {
  1484. transform: rotate(0deg);
  1485. }
  1486. 100% {
  1487. transform: rotate(360deg);
  1488. }
  1489. }
  1490.  
  1491. /* Classe active lors du téléchargement */
  1492. .downloading {
  1493. animation: rotate 1s linear infinite; /* Rotation continue */
  1494. background-color: rgba(0, 0, 0, 0.8); /* Change légèrement le fond */
  1495. border-color: rgba(255, 255, 255, 0.5); /* Bordure plus marquée */
  1496. opacity: 0.7; /* Légère transparence pour indiquer une action */
  1497. }
  1498. `;
  1499. document.head.appendChild(style); // Ajout de la balise style dans le <head> du document
  1500. }
  1501.  
  1502. function addScrollBarStyles() {
  1503. const style = document.createElement('style');
  1504. style.textContent = `
  1505. .thumbnail-scroll-container::-webkit-scrollbar {
  1506. //height: thin;
  1507. height: 7px;
  1508. background-color: transparent;
  1509. }
  1510.  
  1511. .thumbnail-scroll-container::-webkit-scrollbar-button {
  1512. display: none; /* Pas de boutons */
  1513. }
  1514.  
  1515. /* Coin de la scrollbar */
  1516. .thumbnail-scroll-container::-webkit-scrollbar-corner {
  1517. background-color: transparent;
  1518. }
  1519.  
  1520. /* Thumb (la barre de défilement visible) */
  1521. .thumbnail-scroll-container::-webkit-scrollbar-thumb {
  1522. background-color: rgba(74, 77, 82, 0.7);
  1523. border: 2px solid transparent;
  1524. border-radius: 10px;
  1525. }
  1526.  
  1527. /* Thumb au survol (hover) */
  1528. .thumbnail-scroll-container::-webkit-scrollbar-thumb:hover {
  1529. background-color: rgb(90, 93, 98, 0.7);
  1530. }
  1531. `;
  1532.  
  1533. document.head.appendChild(style);
  1534. }
  1535.  
  1536. function injectStyles(){
  1537. const isMobileDevice = isMobile();
  1538.  
  1539. addSpinnerStyles();
  1540. addDownloadButtonStyles();
  1541.  
  1542. if (!isMobileDevice) {
  1543. addScrollBarStyles();
  1544. }
  1545. }
  1546.  
  1547. const parentClasses = `
  1548. .txt-msg,
  1549. .message,
  1550. .conteneur-message.mb-3,
  1551. .bloc-editor-forum,
  1552. .signature-msg,
  1553. .previsu-editor,
  1554. .bloc-description-desc.txt-enrichi-desc-profil,
  1555. .bloc-signature-desc.txt-enrichi-desc-profil
  1556. `.replace(/\s+/g, ''); // Supprimer les sauts de ligne et espaces inutiles
  1557.  
  1558. const linkSelectors = parentClasses.split(',').map(cls => `${cls} a`);
  1559.  
  1560. // Ajouter des écouteurs d'événements aux images sur la page
  1561. function addListeners() {
  1562. linkSelectors.forEach(selector => {
  1563. document.querySelectorAll(selector).forEach(link => {
  1564. link.addEventListener('click', handleImageClick, true);
  1565. });
  1566. });
  1567. }
  1568.  
  1569. function handleImageClick(event) {
  1570. // Si Ctrl ou Cmd est enfoncé, ne pas ouvrir l'ImageViewer
  1571. if (event.ctrlKey || event.metaKey) {
  1572. return;
  1573. }
  1574.  
  1575. const imgElement = this.querySelector('img');
  1576. if (imgElement) {
  1577. event.preventDefault();
  1578. const closestElement = this.closest(parentClasses);
  1579. if (closestElement) {
  1580. const images = Array.from(closestElement.querySelectorAll('a')).filter(imgLink => imgLink.querySelector('img'));
  1581. const currentIndex = images.indexOf(this);
  1582.  
  1583. const viewer = new ImageViewer();
  1584. viewer.openViewer(images, currentIndex);
  1585. }
  1586. }
  1587. }
  1588.  
  1589. function isMobile() {
  1590. return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
  1591. }
  1592.  
  1593. function getImageExtension(url) {
  1594. const match = url.match(/\.(jpg|jpeg|png|gif|bmp|webp|tiff)$/i); // Regexp pour matcher les extensions d'images
  1595. return match ? match[0].toLowerCase() : null;
  1596. }
  1597.  
  1598. // Observer les changements dans le DOM
  1599. function observeDOMChanges() {
  1600. const observer = new MutationObserver(() => addListeners());
  1601. observer.observe(document, { childList: true, subtree: true });
  1602. }
  1603.  
  1604. // Détection des changements d'URL
  1605. function observeURLChanges() {
  1606. let lastUrl = window.location.href;
  1607.  
  1608. const urlObserver = new MutationObserver(() => {
  1609. if (lastUrl !== window.location.href) {
  1610. lastUrl = window.location.href;
  1611. addListeners();
  1612. }
  1613. });
  1614. urlObserver.observe(document, { subtree: true, childList: true });
  1615. }
  1616.  
  1617. function main() {
  1618. injectStyles();
  1619. addListeners();
  1620. observeDOMChanges();
  1621. observeURLChanges();
  1622. }
  1623.  
  1624. main();
  1625.  
  1626. })();