JVC_ImageViewer

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

当前为 2025-01-04 提交的版本,查看 最新版本

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