JVC_ImageViewer

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

当前为 2024-10-11 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name JVC_ImageViewer
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.41.10
  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.indicatorsContainer = null;
  39. this.indicators = [];
  40. this.zoomLevel = 1;
  41. this.isDragging = false;
  42. this.isPinchZooming = false;
  43. this.startX = 0;
  44. this.startY = 0;
  45. this.offsetX = 0;
  46. this.offsetY = 0;
  47. this.xDown = null;
  48. this.yDown = null;
  49. this.initialDistance = null;
  50. this.startTouches = [];
  51. this.isSwiping = false;
  52. this.isScaling = false;
  53. this.imageElementScale = 1;
  54. this.start = {};
  55. this.isMouseDown = false;
  56. this.isTouchDragging = false;
  57. this.dragTimeout = null;
  58. this.mouseDownX = 0;
  59. this.mouseDownY = 0;
  60. this.initialScale = 1;
  61. this.isViewerOpen = false;
  62. this.thumbnailPanel = null;
  63. this.previousThumbnail = null;
  64. this.touchSensitivityFactor = 0.5; // Pour les appareils tactiles
  65. this.mouseSensitivityFactor = 0.4; // Pour les mouvements de souris
  66. this.defaultImageWidth = Math.min(window.innerWidth, 1200);
  67.  
  68. ImageViewer.instance = this;
  69.  
  70. this.handlePopState = this.handlePopState.bind(this);
  71.  
  72. this.createOverlay();
  73. this.updateImage();
  74. }
  75.  
  76. // Crée et configure les éléments du visualiseur d'images (overlay, boutons, texte d'information, etc.)
  77. createOverlay() {
  78. this.overlay = this.createElement('div', {
  79. position: 'fixed',
  80. top: 0,
  81. left: 0,
  82. width: '100%',
  83. height: '100%',
  84. backgroundColor: 'rgba(0, 0, 0, 0.8)',
  85. display: 'flex',
  86. alignItems: 'center',
  87. justifyContent: 'center',
  88. zIndex: 10000
  89. });
  90.  
  91. this.imgElement = this.createElement('img', {
  92. maxWidth: '90%',
  93. maxHeight: '80%',
  94. objectFit: 'contain',
  95. transition: 'opacity 0.3s',
  96. opacity: 0,
  97. cursor: 'pointer',
  98. });
  99.  
  100. this.spinner = this.createSpinner();
  101. this.prevButton = this.createButton('<', 'left');
  102. this.nextButton = this.createButton('>', 'right');
  103. this.closeButton = this.createCloseButton();
  104. this.infoText = this.createInfoText();
  105. this.downloadButton = this.createDownloadButton();
  106.  
  107. this.indicatorsContainer = this.createElement('div', {
  108. display: 'flex',
  109. justifyContent: 'center',
  110. marginBottom: '10px 0',
  111. position: 'absolute',
  112. bottom: '40px',
  113. });
  114.  
  115. // this.addScrollbarStyles();
  116. this.resetHideButtons();
  117.  
  118. // Événements associés aux boutons et à l'overlay
  119. this.addEventListeners();
  120. this.addInteractionListeners();
  121.  
  122. // Ajout des éléments au DOM
  123. //this.overlay.append(this.imgElement, this.spinner, this.infoText, this.prevButton, this.nextButton, this.closeButton);
  124. this.overlay.append(
  125. this.imgElement,
  126. this.spinner,
  127. this.infoText,
  128. this.downloadButton, // Ajout du bouton de téléchargement
  129. this.prevButton,
  130. this.nextButton,
  131. this.closeButton
  132. );
  133. document.body.appendChild(this.overlay);
  134. }
  135.  
  136. // Crée un élément HTML avec des styles
  137. createElement(tag, styles = {}) {
  138. const element = document.createElement(tag);
  139. Object.assign(element.style, styles);
  140. return element;
  141. }
  142.  
  143. // Crée le bouton précédent ou suivant
  144. createButton(text, position) {
  145.  
  146. const isMobileDevice = isMobile();
  147.  
  148. const button = this.createElement('button', {
  149. position: 'absolute',
  150. [position]: '5px',
  151. backgroundColor: 'rgba(0, 0, 0, 0.6)',
  152. color: 'white',
  153. fontSize: isMobileDevice ? '18px' : '22px',//'22px',
  154. border: 'none',
  155. borderRadius: '50%',
  156. width: isMobileDevice ? '35px' : '40px',//'40px',
  157. height: isMobileDevice ? '35px' : '40px',//'40px',
  158. cursor: 'pointer',
  159. display: 'flex',
  160. alignItems: 'center',
  161. justifyContent: 'center',
  162. boxShadow: '0px 4px 8px rgba(0, 0, 0, 0.6)',
  163. transition: 'background-color 0.3s, transform 0.3s'
  164. });
  165.  
  166.  
  167. //button.textContent = text;*
  168. const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
  169. svg.setAttribute('viewBox', '0 0 24 24');
  170. svg.setAttribute('width', '24');
  171. svg.setAttribute('height', '24');
  172. svg.setAttribute('fill', 'white');
  173.  
  174. const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  175. path.setAttribute('d', position === 'left'
  176. ? 'M15.41 16.59L10.83 12l4.58-4.59L14 6l-6 6 6 6z' // Icône flèche gauche
  177. : 'M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6z'); // Icône flèche droite
  178. svg.appendChild(path);
  179. button.appendChild(svg);
  180.  
  181. this.addButtonEffects(button);
  182.  
  183. return button;
  184. }
  185.  
  186. createDownloadButton() {
  187. const isMobileDevice = isMobile();
  188.  
  189. const button = this.createElement('button', {
  190. position: 'absolute',
  191. top: '120px',
  192. left: '15px',
  193. backgroundColor: 'rgba(0, 0, 0, 0.6)',
  194. color: 'white',
  195. fontSize: isMobileDevice ? '12px' : '10px',
  196. border: '1px solid rgba(255, 255, 255, 0.3)',
  197. borderRadius: '50%',
  198. padding: '0',
  199. cursor: 'pointer',
  200. zIndex: 10001,
  201. display: 'flex',
  202. alignItems: 'center',
  203. justifyContent: 'center',
  204. width: isMobileDevice ? '37px' : '45px',
  205. height: isMobileDevice ? '37px' : '45px',
  206. boxShadow: '0 2px 4px rgba(0, 0, 0, 0.5)',
  207. transition: 'transform 0.3s ease, background-color 0.3s ease',
  208. });
  209.  
  210. const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
  211. svg.setAttribute('viewBox', '0 0 24 24');
  212. svg.setAttribute('width', isMobileDevice ? '22' : '26');
  213. svg.setAttribute('height', isMobileDevice ? '22' : '26');
  214. svg.setAttribute('fill', 'white');
  215.  
  216. // Nouvelle flèche épurée vers le bas
  217. const arrowPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  218. arrowPath.setAttribute('d', 'M12 16l-4-4h3V4h2v8h3l-4 4z');
  219. svg.appendChild(arrowPath);
  220.  
  221. // Barre horizontale plus discrète représentant le disque dur
  222. const diskPath = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
  223. diskPath.setAttribute('x', '8');
  224. diskPath.setAttribute('y', '18');
  225. diskPath.setAttribute('width', '8');
  226. diskPath.setAttribute('height', '2');
  227. svg.appendChild(diskPath);
  228.  
  229. button.appendChild(svg);
  230. this.addButtonEffects(button);
  231.  
  232. return button;
  233. }
  234.  
  235.  
  236. // Crée le bouton de fermeture
  237. createCloseButton() {
  238. const isMobileDevice = isMobile();
  239.  
  240. const button = this.createElement('button', {
  241. position: 'absolute',
  242. top: '80px',
  243. right: '10px',
  244. backgroundColor: 'rgba(0, 0, 0, 0.8)',
  245. color: 'white',
  246. fontSize: isMobileDevice ? '18px' : '16px',
  247. //border: 'none',
  248. border: '1px solid rgba(255, 255, 255, 0.3)',
  249. borderRadius: '50%',
  250. width: isMobileDevice ? '40px' : '35px',
  251. height: isMobileDevice ? '40px' : '35px',
  252. cursor: 'pointer',
  253. zIndex: 99999999
  254. });
  255.  
  256. button.textContent = '✕';
  257. this.addButtonEffects(button);
  258.  
  259. return button;
  260. }
  261.  
  262. // Crée la zone d'affichage du texte d'information (numéro d'image)
  263. createInfoText() {
  264. return this.createElement('div', {
  265. position: 'absolute',
  266. top: '80px',
  267. left: '15px',
  268. color: 'white',
  269. fontSize: '12px',
  270. backgroundColor: 'rgba(5, 5, 5, 0.5)',
  271. padding: '5px',
  272. borderRadius: '5px',
  273. zIndex: 10001
  274. });
  275. }
  276.  
  277. // Crée un spinner pour indiquer le chargement de l'image
  278. createSpinner() {
  279. const spinner = this.createElement('div', {
  280. position: 'absolute',
  281. border: '8px solid #f3f3f3',
  282. borderTop: '8px solid #3498db',
  283. borderRadius: '50%',
  284. width: '50px',
  285. height: '50px',
  286. animation: 'spin 1s linear infinite',
  287. zIndex: 10001
  288. });
  289. return spinner;
  290. }
  291.  
  292.  
  293. addButtonEffects(button) {
  294. button.addEventListener('mouseenter', () => {
  295. button.style.backgroundColor = 'rgba(255, 255, 255, 0.8)';
  296. button.style.color = 'black';
  297. button.style.transform = 'scale(1.1)';
  298. });
  299.  
  300. button.addEventListener('mouseleave', () => {
  301. button.style.backgroundColor = 'rgba(0, 0, 0, 0.6)';
  302. button.style.color = 'white';
  303. button.style.transform = 'scale(1)';
  304. });
  305.  
  306. button.addEventListener('mousedown', () => {
  307. button.style.transform = 'scale(0.9)';
  308. });
  309.  
  310. button.addEventListener('mouseup', () => {
  311. button.style.transform = 'scale(1.1)';
  312. });
  313. }
  314.  
  315. // Ajoute les événements aux différents éléments du visualiseur
  316. addEventListeners() {
  317. // Bouttons de controles du visualiseur
  318. this.prevButton.addEventListener('click', () => this.changeImage(-1));
  319. this.nextButton.addEventListener('click', () => this.changeImage(1));
  320. this.closeButton.addEventListener('click', () => this.closeViewer());
  321. this.downloadButton.addEventListener('click', () => this.startDownload());
  322. this.overlay.addEventListener('click', (event) => {
  323. if (event.target === this.overlay) {
  324. this.closeViewer();
  325. }
  326. });
  327.  
  328. // Zoom avec la molette de la souris
  329. this.imgElement.addEventListener('wheel', (event) => this.handleZoom(event));
  330.  
  331.  
  332. // Déplacement lors du zoom (drag)
  333. this.imgElement.addEventListener('mousedown', (event) => this.startDrag(event));
  334. this.imgElement.addEventListener('mousedown', this.handleMouseDown.bind(this));
  335. this.imgElement.addEventListener('mouseup', this.handleMouseUp.bind(this));
  336.  
  337. // Touches avec les doigts
  338. this.imgElement.addEventListener('touchstart', (event) => this.handleTouchEvent(event));
  339. this.imgElement.addEventListener('touchmove', (event) => this.handleTouchEvent(event));
  340. this.imgElement.addEventListener('touchend', (event) => this.handleTouchEvent(event));
  341.  
  342. // Ouvrir l'image dans une no6velle fenêtre
  343. this.imgElement.addEventListener('click', () => {
  344. if (!this.isDragging) {
  345. window.open(this.images[this.currentIndex].href, '_blank');
  346. }
  347. });
  348.  
  349. // Touches du clavier
  350. document.addEventListener('keydown', (event) => this.handleKeyboardEvents(event));
  351. }
  352.  
  353. // Fonctions pour gérer les touches du clavier
  354. handleKeyboardEvents(event) {
  355. switch (event.key) {
  356. case 'ArrowLeft':
  357. case 'ArrowUp':
  358. this.changeImage(-1);
  359. break;
  360. case 'ArrowRight':
  361. case 'ArrowDown':
  362. this.changeImage(1);
  363. break;
  364. case 'Escape':
  365. event.preventDefault();
  366. this.closeViewer();
  367. break;
  368. }
  369. }
  370.  
  371. // Fonctions pour gérer les touches tactiles
  372. handleTouchEvent(event) {
  373. switch (event.type) {
  374. case 'touchstart':
  375. if (event.touches.length === 1) {
  376. if (this.imageElementScale > 1) {
  377. // Si l'image est zoomée, permettre le déplacement (drag)
  378. this.startDrag(event);
  379. } else {
  380. // Sinon, démarrer le swipe
  381. console.log("swipe start");
  382. this.handleSwipeStart(event);
  383. }
  384. } else if (event.touches.length === 2) {
  385. // Démarrer le pinch zoom
  386. this.handlePinchStart(event);
  387. }
  388. break;
  389.  
  390. case 'touchmove':
  391. if (event.touches.length === 1) {
  392. if (this.imageElementScale > 1) {
  393. // Si l'image est zoomée, permettre le déplacement (drag)
  394. this.onDrag(event);
  395. } else {
  396. console.log("swipe move");
  397. this.handleSwipeMove(event);
  398. }
  399. } else if (event.touches.length === 2) {
  400. // Gérer le pinch zoom
  401. this.handlePinchMove(event);
  402. }
  403. break;
  404.  
  405. case 'touchend':
  406. if (event.touches.length === 1) {
  407. if (this.imageElementScale > 1) {
  408. this.endDrag(event);
  409. }
  410. }
  411. else if (event.touches.length === 0) {
  412. if (this.isSwiping) {
  413. this.handleSwipeEnd(event);
  414. } else if (this.isPinchZooming) {
  415. this.handlePinchEnd(event);
  416. } else {
  417. this.endDrag(event);
  418. }
  419. }
  420. break;
  421. }
  422. }
  423.  
  424.  
  425. // Gestion du début de l'interaction tactile
  426. handleSwipeStart(event) {
  427. if (event.touches.length === 1) {
  428. if(this.isPinchZooming) {
  429. return;
  430. }
  431. // Commencer le swipe
  432. this.isSwiping = true;
  433. this.startX = event.touches[0].clientX;
  434. this.startY = event.touches[0].clientY;
  435. }
  436. }
  437.  
  438. // Gestion du mouvement tactile pour swipe
  439. handleSwipeMove(event) {
  440. if (this.isSwiping && event.touches.length === 1) {
  441. this.currentX = event.touches[0].clientX;
  442. this.currentY = event.touches[0].clientY;
  443. }
  444. }
  445.  
  446. // Gestion de la fin de l'interaction tactile
  447. handleSwipeEnd(event) {
  448. if (event.touches.length === 0) {
  449. this.initialDistance = null;
  450. }
  451. if (this.isSwiping) {
  452. this.isSwiping = false;
  453. const deltaX = this.currentX - this.startX;
  454. const deltaY = this.currentY - this.startY;
  455.  
  456. // Si le mouvement est suffisamment grand, on change d'image
  457. if (Math.abs(deltaX) > 50) {
  458. if (deltaX > 0) {
  459. this.showPreviousImage();
  460. } else {
  461. this.showNextImage();
  462. }
  463. }
  464.  
  465. // Si le mouvement est suffisamment grand verticalement, on ferme le visualiseur
  466. if (Math.abs(deltaY) > 50) {
  467. this.closeViewer();
  468. }
  469. }
  470.  
  471. // l'image revient à sa taille d'origine, réinitialiser le zIndex
  472. this.imgElement.style.zIndex = '';
  473. }
  474.  
  475. // Calculate distance between two fingers
  476. distance(event){
  477. return Math.hypot(event.touches[0].pageX - event.touches[1].pageX, event.touches[0].pageY - event.touches[1].pageY);
  478. }
  479.  
  480. // Gestion du début de l'interaction tactile pour le pincement
  481. handlePinchStart(event) {
  482. if (event.touches.length === 2) {
  483. event.preventDefault(); // Prevent page scroll
  484. this.isPinchZooming = true;
  485.  
  486. // Calculate where the fingers have started on the X and Y axis
  487. this.start.x = (event.touches[0].pageX + event.touches[1].pageX) / 2;
  488. this.start.y = (event.touches[0].pageY + event.touches[1].pageY) / 2;
  489. this.start.distance = this.distance(event);
  490.  
  491. // Save the current scale to use it as a base for the new scale
  492. this.initialScale = this.imageElementScale || 1; // Use 1 if there's no previous zoom
  493. }
  494. }
  495.  
  496. // Gestion du mouvement tactile pour le pincement (zoom)
  497. handlePinchMove(event) {
  498. if (event.touches.length === 2) {
  499. event.preventDefault(); // Prevent page scroll
  500.  
  501. // Safari provides event.scale as two fingers move on the screen
  502. // For other browsers just calculate the scale manually
  503. let scale;
  504. if (event.scale) {
  505. scale = event.scale;
  506. } else {
  507. const deltaDistance = this.distance(event);
  508. scale = (deltaDistance / this.start.distance); //* this.touchSensitivityFactor;
  509. }
  510. // this.imageElementScale = Math.min(Math.max(1, scale), 4);
  511. // Multiply the new scale by the starting scale to retain the zoom level
  512. // this.imageElementScale = Math.min(Math.max(1, this.startScale * scale), 4);
  513. this.imageElementScale = Math.min(Math.max(1, this.initialScale * scale), 4);
  514.  
  515.  
  516. // Calculate how much the fingers have moved on the X and Y axis
  517. const deltaX = (((event.touches[0].pageX + event.touches[1].pageX) / 2) - this.start.x) * 2; // x2 for accelarated movement
  518. const deltaY = (((event.touches[0].pageY + event.touches[1].pageY) / 2) - this.start.y) * 2; // x2 for accelarated movement
  519.  
  520. // Transform the image to make it grow and move with fingers
  521. const transform = `translate3d(${deltaX}px, ${deltaY}px, 0) scale(${this.imageElementScale})`;
  522. this.imgElement.style.transform = transform;
  523. this.imgElement.style.WebkitTransform = transform;
  524. this.imgElement.style.zIndex = "9999";
  525. this.closeButton.style.zIndex = "10003";
  526. }
  527. }
  528.  
  529. // Gestion de la fin de l'interaction tactile pour le pincement
  530. handlePinchEnd(event) {
  531. if (event.touches.length < 2) {
  532. // Ajouter un délai pour réinitialiser le drag (empeche les conflits avec le clic)
  533. this.dragTimeout = setTimeout(() => {
  534. this.isPinchZooming = false;
  535. this.dragTimeout = null;
  536. }, 100);
  537.  
  538. // Si l'image est revenue à sa taille d'origine, réinitialiser le zIndex
  539. if (this.imageElementScale <= 1) {
  540. this.imgElement.style.zIndex = '';
  541. }
  542. }
  543. }
  544.  
  545. // Fonction pour calculer la distance entre deux points de contact
  546. getTouchDistance(touches) {
  547. const [touch1, touch2] = touches;
  548. const deltaX = touch2.clientX - touch1.clientX;
  549. const deltaY = touch2.clientY - touch1.clientY;
  550. return Math.sqrt(deltaX * deltaX + deltaY * deltaY);
  551. }
  552.  
  553. handleZoom(event) {
  554. event.preventDefault();
  555. const zoomIncrement = 0.07;
  556.  
  557. /*if (event.deltaY < 0) {
  558. this.imageElementScale += zoomIncrement; // Zoomer
  559. } else {
  560. this.imageElementScale = Math.max(1, this.zoomLevel - zoomIncrement); // Dézoomer, mais ne pas descendre sous 1
  561. }*/
  562. if (event.deltaY < 0) {
  563. this.imageElementScale = Math.min(4, this.imageElementScale + zoomIncrement); // Limite max
  564. } else {
  565. this.imageElementScale = Math.max(1, this.imageElementScale - zoomIncrement); // Limite min
  566. }
  567.  
  568. this.imgElement.style.transform = `scale(${this.imageElementScale}) translate(${this.offsetX}px, ${this.offsetY}px)`;
  569.  
  570. // Si le niveau de zoom est supérieur à 1, mettre l'image devant les boutons
  571. if (this.zoomLevel > 1) {
  572. this.imgElement.style.zIndex = 10002;
  573. } else {
  574. // Si le zoom revient à la normale, remettre le zIndex initial
  575. this.imgElement.style.zIndex = '';
  576. }
  577. }
  578.  
  579.  
  580. startDrag(event) {
  581. event.preventDefault(); // Empêche la sélection de l'image
  582.  
  583. // Gestion tactile
  584. if (event.type === 'touchstart') {
  585. this.isTouchDragging = true;
  586. this.startX = event.touches[0].clientX; //- this.offsetX;
  587. this.startY = event.touches[0].clientY; //- this.offsetY;
  588. } else {
  589. // Gestion avec la souris
  590. this.isMouseDown = true;
  591. this.startX = event.clientX; //- this.offsetX;
  592. this.startY = event.clientY; // - this.offsetY;
  593. }
  594.  
  595. this.isDragging = true;
  596. this.imgElement.style.cursor = 'grabbing';
  597. this.imgElement.style.userSelect = 'none';
  598.  
  599. // Ajouter des listeners sur le document pour capturer les mouvements
  600. if (event.touches) {
  601. document.addEventListener('touchmove', this.onDragBound = this.onDrag.bind(this));
  602. document.addEventListener('touchend', this.endDragBound = this.endDrag.bind(this));
  603. } else {
  604. document.addEventListener('mousemove', this.onDragBound = this.onDrag.bind(this));
  605. document.addEventListener('mouseup', this.endDragBound = this.endDrag.bind(this));
  606. }
  607. }
  608.  
  609.  
  610. onDrag(event) {
  611. if (!this.isDragging) return;
  612.  
  613. event.preventDefault();
  614.  
  615. let deltaX, deltaY;
  616.  
  617. if (event.type === 'touchmove') {
  618. // Gestion tactile
  619. deltaX = (event.touches[0].clientX - this.startX) * this.touchSensitivityFactor;
  620. deltaY = (event.touches[0].clientY - this.startY) * this.touchSensitivityFactor;
  621. } else {
  622. // Gestion avec la souris
  623. deltaX = (event.clientX - this.startX) * this.mouseSensitivityFactor;
  624. deltaY = (event.clientY - this.startY) * this.mouseSensitivityFactor;
  625. }
  626.  
  627. // Appliquer la translation ajustée par le facteur de sensibilité
  628. this.offsetX += deltaX;
  629. this.offsetY += deltaY;
  630.  
  631. // Mettre à jour les points de départ pour le prochain déplacement
  632. this.startX = event.type === 'touchmove' ? event.touches[0].clientX : event.clientX;
  633. this.startY = event.type === 'touchmove' ? event.touches[0].clientY : event.clientY;
  634.  
  635. // Appliquer la transformation avec le zoom actuel, en se déplaçant dans l'image
  636. this.imgElement.style.transform = `scale(${this.imageElementScale}) translate(${this.offsetX}px, ${this.offsetY}px)`;
  637. }
  638.  
  639.  
  640. endDrag(event) {
  641. this.imgElement.style.cursor = 'grab';
  642.  
  643. // Retirer les listeners
  644. if (event.type === 'touchend') {
  645. this.isTouchDragging = false; // Réinitialise l'état tactile
  646. document.removeEventListener('touchmove', this.onDragBound);
  647. document.removeEventListener('touchend', this.endDragBound);
  648. } else {
  649. this.isMouseDown = false; // Réinitialise l'état de la souris
  650. document.removeEventListener('mousemove', this.onDragBound);
  651. document.removeEventListener('mouseup', this.endDragBound);
  652. }
  653.  
  654. // Ajouter un délai pour réinitialiser le drag (empeche les conflits avec le clic)
  655. this.dragTimeout = setTimeout(() => {
  656. this.isDragging = false;
  657. this.imgElement.style.cursor = 'pointer';
  658. this.dragTimeout = null;
  659. }, 100);
  660. }
  661.  
  662.  
  663. handleMouseDown(event) {
  664. this.isMouseDown = true;
  665. this.mouseDownX = event.clientX;
  666. this.mouseDownY = event.clientY;
  667.  
  668. // Démarrer le drag après un délai pour éviter le drag lors des clics courts
  669. this.dragTimeout = setTimeout(() => {
  670. if (this.isMouseDown) {
  671. this.startDrag(event);
  672. }
  673. }, 200);
  674. }
  675.  
  676. handleMouseUp(event) {
  677. this.isMouseDown = false;
  678.  
  679. // Si le délai pour démarrer le drag est encore en cours, le nettoyer
  680. if (this.dragTimeout) {
  681. clearTimeout(this.dragTimeout);
  682. this.dragTimeout = null;
  683. }
  684.  
  685. // Vérifier si le mouvement est suffisant pour considérer que c'est un drag
  686. const movedX = Math.abs(event.clientX - this.mouseDownX);
  687. const movedY = Math.abs(event.clientY - this.mouseDownY);
  688.  
  689. if (movedX < 5 && movedY < 5) {
  690. // handleImageClick(event); // Traiter le clic si le mouvement est minime
  691. }
  692. }
  693.  
  694. // Réinitialiser le zoom de l'image
  695. resetZoom() {
  696. this.imgElement.style.transform = 'scale(1)';
  697. this.imgElement.style.transformOrigin = '0 0';
  698. this.imageElementScale = 1;
  699. this.offsetX = 0;
  700. this.offsetY = 0;
  701. }
  702.  
  703. // Réinitialiser la position du drag de l'image
  704. resetDrag() {
  705. this.imgElement.style.left = '0px';
  706. this.imgElement.style.top = '0px';
  707. }
  708.  
  709. // Met à jour l'image affichée dans le visualiseur
  710. updateImage() {
  711. if (this.currentIndex >= 0 && this.currentIndex < this.images.length) {
  712. const imageUrl = this.images[this.currentIndex].href;
  713.  
  714. this.imgElement.src = imageUrl;
  715. this.infoText.textContent = `${this.currentIndex + 1} / ${this.images.length}`;
  716. this.spinner.style.display = 'block';
  717.  
  718. this.toggleButtonState();
  719.  
  720. this.imgElement.onload = () => {
  721. this.imgElement.style.opacity = 1;
  722. this.spinner.style.display = 'none';
  723.  
  724. // Réinitialiser le zoom et la position du drag
  725. this.resetZoom();
  726. this.resetDrag();
  727.  
  728. // Calcul de la position des boutons
  729. const imgRect = this.imgElement.getBoundingClientRect();
  730. const isMobileDevice = isMobile(); // Détection des mobiles
  731.  
  732. if (imgRect.width > this.defaultImageWidth) {
  733. this.defaultImageWidth = imgRect.width;
  734. }
  735.  
  736. if (isMobileDevice) {
  737. // pass
  738. } else {
  739. const margin = 30;
  740. // Calcul de la position des boutons
  741. let prevButtonLeft = (window.innerWidth - this.defaultImageWidth) / 2 - this.prevButton.offsetWidth - margin;
  742. let nextButtonRight = (window.innerWidth - this.defaultImageWidth) / 2 - this.nextButton.offsetWidth - margin;
  743.  
  744. // Limite les boutons pour qu'ils ne sortent pas de l'écran à gauche ou à droite
  745. prevButtonLeft = Math.max(prevButtonLeft, margin); // Empêche de sortir à gauche
  746. nextButtonRight = Math.max(nextButtonRight, margin); // Empêche de sortir à droite
  747.  
  748. // Appliquer les positions ajustées
  749. this.prevButton.style.left = `${prevButtonLeft}px`;
  750. this.nextButton.style.right = `${nextButtonRight}px`;
  751. }
  752.  
  753. this.focusOnThumbnail();
  754. };
  755.  
  756. this.imgElement.onerror = () => this.handleImageError();
  757. }
  758. }
  759.  
  760. // Gestion des erreurs de chargement d'image
  761. handleImageError() {
  762. const miniUrl = this.images[this.currentIndex].querySelector('img').src;
  763. const fullUrl = this.images[this.currentIndex].href;
  764. const extensions = this.reorderExtensions(fullUrl);
  765. const baseUrl = miniUrl.replace('/minis/', '/fichiers/');
  766.  
  767. const tryNextExtension = (index) => {
  768. if (index >= extensions.length) {
  769. // Si toutes les extensions échouent, tenter l'URL originale (mini)
  770. this.imgElement.src = miniUrl;
  771. return;
  772. }
  773.  
  774. // Remplacer l'extension et mettre à jour l'URL
  775. const updatedUrl = baseUrl.replace(/\.(jpg|png|jpeg)$/, extensions[index]);
  776.  
  777. // Tester l'URL avec un élément Image temporaire
  778. const imgTest = new Image();
  779. imgTest.src = updatedUrl;
  780.  
  781. imgTest.onload = () => {
  782. // Si l'image se charge avec succès, l'assigner à l'élément d'image principal
  783. this.imgElement.src = updatedUrl;
  784. };
  785.  
  786. imgTest.onerror = () => {
  787. // console.log("Error loading: " + updatedUrl);
  788. tryNextExtension(index + 1);
  789. };
  790. };
  791.  
  792. // Commencer les essais avec la première extension
  793. tryNextExtension(0);
  794. }
  795.  
  796. // Réarranger la liste des extensions a tester pour mettre l'extension utilisée sur noelshack en premier
  797. reorderExtensions(currentImageUrl) {
  798. const extensions = ['.jpg', '.png', '.jpeg'];
  799. const currentExtension = getImageExtension(currentImageUrl);
  800. const newExtensions = [...extensions];
  801.  
  802. if (currentExtension) {
  803. if (!newExtensions.includes(currentExtension)) {
  804. newExtensions.unshift(currentExtension);
  805. } else {
  806. const index = newExtensions.indexOf(currentExtension);
  807. if (index > -1) {
  808. newExtensions.splice(index, 1);
  809. newExtensions.unshift(currentExtension);
  810. }
  811. }
  812. }
  813. return newExtensions; // Retourne la liste réorganisée
  814. }
  815.  
  816. // Change d'image en fonction de la direction (suivant/précédent)
  817. changeImage(direction) {
  818. this.currentIndex = (this.currentIndex + direction + this.images.length) % this.images.length;
  819. this.imgElement.style.opacity = 0;
  820. this.spinner.style.display = 'block';
  821. this.updateImage();
  822. }
  823.  
  824. showPreviousImage() {
  825. this.changeImage(-1);
  826. }
  827.  
  828. showNextImage() {
  829. this.changeImage(1);
  830. }
  831.  
  832. // Met à jour le focus sur la miniature correspondante
  833. focusOnThumbnail() {
  834. // Obtenez la miniature actuelle
  835. const thumbnails = this.thumbnailPanel ? this.thumbnailPanel.querySelectorAll('img') : [];
  836. const currentThumbnail = thumbnails[this.currentIndex];
  837.  
  838. // Réinitialiser la bordure de la miniature précédente si elle existe
  839. if (this.previousThumbnail) {
  840. this.previousThumbnail.style.border = 'none';
  841. this.previousThumbnail.style.transform = 'scale(1)';
  842. this.previousThumbnail.style.boxShadow = 'none';
  843. }
  844.  
  845. // Mettre à jour la bordure de la miniature actuelle
  846. if (currentThumbnail) {
  847. //currentThumbnail.style.border = '2px solid rgba(40, 40, 40, 0.8)'; // Appliquer la bordure
  848. currentThumbnail.style.boxShadow = '0 0 0 2px rgba(40, 40, 40, 0.8), 0 0 10px rgba(40, 40, 40, 0.5)'; // Ombre portée
  849. currentThumbnail.style.transform = 'scale(1.15)'; // Agrandir l'élément
  850. currentThumbnail.parentElement.scrollIntoView({ behavior: 'smooth', inline: 'center' });
  851. }
  852.  
  853. // Mettre à jour la référence de la miniature précédente
  854. this.previousThumbnail = currentThumbnail;
  855. }
  856.  
  857. // Désactive ou active les boutons suivant/précédent en fonction de l'index actuel
  858. toggleButtonState() {
  859. if (this.currentIndex === 0) {
  860. // this.prevButton.disabled = true;
  861. this.prevButton.style.opacity = 0.5;
  862. this.prevButton.style.cursor = 'initial';
  863. } else {
  864. // this.prevButton.disabled = false;
  865. this.prevButton.style.opacity = 1;
  866. this.prevButton.style.cursor = 'pointer';
  867. }
  868.  
  869. if (this.currentIndex === this.images.length - 1) {
  870. // this.nextButton.disabled = true;
  871. this.nextButton.style.opacity = 0.5;
  872. this.nextButton.style.cursor = 'initial';
  873. } else {
  874. // this.nextButton.disabled = false;
  875. this.nextButton.style.opacity = 1;
  876. this.nextButton.style.cursor = 'pointer';
  877. }
  878. }
  879.  
  880. // Cacher temporairement le menu JVC
  881. toggleMenuVisibility(isVisible) {
  882. const menu = document.querySelector('.header__bottom');
  883. if (menu) {
  884. menu.style.display = isVisible ? '' : 'none';
  885. }
  886. }
  887.  
  888. // Fonction pour créer et afficher le panneau des miniatures
  889. toggleThumbnailPanel() {
  890. if (this.thumbnailPanel) {
  891. this.closeThumbnailPanel(); // Ferme le panneau si déjà ouvert
  892. return;
  893. }
  894.  
  895. // Créer le panneau
  896. this.thumbnailPanel = this.createElement('div', {
  897. position: 'fixed',
  898. bottom: '10px',
  899. left: '50%',
  900. transform: 'translateX(-50%)',
  901. border: '0px solid',
  902. padding: '0px',
  903. zIndex: '99999999',
  904. maxHeight: '80px',
  905. maxWidth: '80%',
  906. overflowY: 'hidden',
  907. overflowX: 'auto',
  908. display: 'flex',
  909. alignItems: 'center',
  910. backgroundColor: 'transparent',
  911. });
  912. this.thumbnailPanel.classList.add('thumbnail-scroll-container');
  913.  
  914. // Conteneur pour le défilement horizontal
  915. const scrollContainer = this.createElement('div', {
  916. display: 'flex',
  917. overflowX: 'auto',
  918. whiteSpace: 'nowrap',
  919. maxWidth: '100%',
  920. });
  921.  
  922. // Ajout des images au conteneur
  923. this.images.forEach((image, index) => {
  924. const imgContainer = this.createElement('div', {
  925. display: 'inline-block',
  926. width: '50px',
  927. height: '50px',
  928. margin: '5px',
  929. padding: '4px',
  930. transition: 'transform 0.3s',
  931. });
  932.  
  933. const imgElement = this.createElement('img');
  934. imgElement.src = image.querySelector('img') ? image.querySelector('img').src : image.href || image.thumbnail;
  935.  
  936. imgElement.alt = `Image ${index + 1}`;
  937. imgElement.style.width = 'auto';
  938. imgElement.style.height = '100%';
  939. imgElement.style.objectFit = 'cover';
  940. imgElement.style.cursor = 'pointer';
  941.  
  942. imgElement.addEventListener('click', () => {
  943. this.images.forEach((_, i) => {
  944. const container = scrollContainer.children[i];
  945. container.querySelector('img').style.border = 'none';
  946. });
  947. //imgElement.style.border = '2px solid blue';
  948. this.currentIndex = index;
  949. this.updateImage();
  950. //imgContainer.scrollIntoView({ behavior: 'smooth', inline: 'center' });
  951. });
  952.  
  953. imgContainer.appendChild(imgElement);
  954. scrollContainer.appendChild(imgContainer);
  955. });
  956.  
  957. this.thumbnailPanel.appendChild(scrollContainer);
  958. this.overlay.appendChild(this.thumbnailPanel);
  959.  
  960. this.focusOnThumbnail();
  961. }
  962.  
  963.  
  964. // Ecouteurs d'événements pour réinitialiser le timer
  965. addInteractionListeners() {
  966. this.overlay.addEventListener('mousemove', this.resetHideButtons.bind(this));
  967. this.overlay.addEventListener('click', this.resetHideButtons.bind(this));
  968. this.overlay.addEventListener('touchstart', this.resetHideButtons.bind(this));
  969. }
  970.  
  971. // Réinitialisez le timer pour cacher les boutons
  972. resetHideButtons() {
  973. if (this.hideButtonsTimeout) {
  974. clearTimeout(this.hideButtonsTimeout);
  975. }
  976. this.toggleButtonsVisibility(true);
  977. this.hideButtonsTimeout = setTimeout(() => {
  978. this.toggleButtonsVisibility(false); // Cachez les boutons après 3 secondes
  979. }, 2500);
  980. }
  981.  
  982. // Changez la visibilité des boutons
  983. toggleButtonsVisibility(visible) {
  984. const displayValue = visible ? 'flex' : 'none';
  985.  
  986. const elements = [
  987. this.prevButton,
  988. this.nextButton,
  989. this.thumbnailPanel,
  990. this.infoText,
  991. this.downloadButton
  992. ];
  993.  
  994. elements.forEach(element => {
  995. if (element) {
  996. element.style.display = displayValue;
  997. }
  998. });
  999. }
  1000.  
  1001. startDownload() {
  1002. this.downloadButton.classList.add('downloading'); // Ajout de la classe pour l'animation
  1003.  
  1004. this.downloadCurrentImage().then(() => {
  1005. // Retirer la classe après le téléchargement
  1006. this.downloadButton.classList.remove('downloading');
  1007. }).catch((error) => {
  1008. console.error('Download failed:', error);
  1009. this.downloadButton.classList.remove('downloading');
  1010. });
  1011. }
  1012.  
  1013. downloadCurrentImage() {
  1014. return new Promise((resolve, reject) => {
  1015. const imageElement = this.imgElement;
  1016. if (!imageElement) {
  1017. console.error('Image not found!');
  1018. reject('Image not found');
  1019. return;
  1020. }
  1021.  
  1022. const imageUrl = imageElement.src;
  1023. const fileNameWithExtension = imageUrl.split('/').pop();
  1024. const fileName = fileNameWithExtension.substring(0, fileNameWithExtension.lastIndexOf('.'));
  1025.  
  1026. // Utilisation de GM.xmlHttpRequest pour contourner CORS
  1027. GM.xmlHttpRequest({
  1028. method: "GET",
  1029. url: imageUrl,
  1030. responseType: "blob",
  1031. headers: {
  1032. 'Accept': 'image/jpeg,image/png,image/gif,image/bmp,image/tiff,image/*;q=0.8'
  1033. },
  1034. onload: function(response) {
  1035. if (response.status === 200) {
  1036. const blob = response.response;
  1037. const url = URL.createObjectURL(blob);
  1038.  
  1039. // Téléchargement du fichier
  1040. const a = document.createElement('a');
  1041. a.href = url;
  1042. a.download = fileName;
  1043. document.body.appendChild(a);
  1044. a.click();
  1045. document.body.removeChild(a);
  1046. URL.revokeObjectURL(url);
  1047.  
  1048. resolve(); // Indique que le téléchargement est terminé
  1049. } else {
  1050. reject('Error downloading image: ' + response.statusText);
  1051. }
  1052. },
  1053. onerror: function(err) {
  1054. reject('Request failed: ' + err);
  1055. }
  1056. });
  1057. });
  1058. }
  1059.  
  1060. // Fonction pour fermer le panneau des miniatures
  1061. closeThumbnailPanel(thumbnailPanel) {
  1062. if (this.thumbnailPanel && this.overlay.contains(this.thumbnailPanel)) {
  1063. this.overlay.removeChild(this.thumbnailPanel);
  1064. this.thumbnailPanel = null;
  1065. }
  1066. }
  1067.  
  1068. closeViewer() {
  1069. if (this.overlay) {
  1070. this.handleCloseViewer(); // Ferme le visualiseur
  1071. history.back(); // Supprime l'état ajouté par pushState
  1072. }
  1073. }
  1074.  
  1075.  
  1076. // Ferme le visualiseur d'images
  1077. handleCloseViewer() {
  1078. if (this.overlay) {
  1079. document.body.removeChild(this.overlay);
  1080.  
  1081. // Ferme le panneau des miniatures si ouvert
  1082. if (this.thumbnailPanel) {
  1083. this.closeThumbnailPanel(this.thumbnailPanel);
  1084. }
  1085.  
  1086. window.removeEventListener('popstate', this.handlePopState);
  1087.  
  1088. this.overlay = null;
  1089. this.isViewerOpen = false;
  1090. ImageViewer.instance = null; // Réinitialise l'instance singleton
  1091.  
  1092. this.toggleMenuVisibility(true);
  1093. }
  1094. }
  1095.  
  1096. openViewer(images, currentIndex) {
  1097. if (this.overlay) {
  1098. this.images = images;
  1099. this.currentIndex = currentIndex;
  1100. this.updateImage();
  1101. this.toggleThumbnailPanel();
  1102. } else {
  1103. new ImageViewer();
  1104. this.images = images;
  1105. this.currentIndex = currentIndex;
  1106. this.createOverlay();
  1107. this.updateImage();
  1108. this.toggleThumbnailPanel();
  1109. }
  1110. this.isViewerOpen = true;
  1111.  
  1112. this.addHistoryState()
  1113. window.addEventListener('popstate', this.handlePopState); // Ecouter l'événement bouton back du navigateur
  1114.  
  1115. this.toggleMenuVisibility(false);
  1116. }
  1117.  
  1118. handlePopState(event) {
  1119. if (ImageViewer.instance) {
  1120. event.preventDefault();
  1121. this.handleCloseViewer();
  1122. }
  1123. }
  1124.  
  1125. // Ajouter une entrée dans l'historique
  1126. addHistoryState() {
  1127. history.pushState({ viewerOpen: true }, '');
  1128. }
  1129. }
  1130.  
  1131. function addSpinnerStyles() {
  1132. const style = document.createElement('style');
  1133. style.textContent = `
  1134. @keyframes spin {
  1135. 0% { transform: rotate(0deg); }
  1136. 100% { transform: rotate(360deg); }
  1137. }
  1138. .spinner {
  1139. width: 50px;
  1140. height: 50px;
  1141. border-radius: 50%;
  1142. border: 5px solid transparent;
  1143. border-top: 5px solid rgba(0, 0, 0, 0.1);
  1144. background: conic-gradient(from 0deg, rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0));
  1145. animation: spin 1s linear infinite;
  1146. }
  1147. `;
  1148. document.head.appendChild(style);
  1149. }
  1150.  
  1151. function addDownloadButtonStyles() {
  1152. const style = document.createElement('style');
  1153. style.textContent = `
  1154. /* Animation de rotation */
  1155. @keyframes rotate {
  1156. 0% {
  1157. transform: rotate(0deg);
  1158. }
  1159. 100% {
  1160. transform: rotate(360deg);
  1161. }
  1162. }
  1163.  
  1164. /* Classe active lors du téléchargement */
  1165. .downloading {
  1166. animation: rotate 1s linear infinite; /* Rotation continue */
  1167. background-color: rgba(0, 0, 0, 0.8); /* Change légèrement le fond */
  1168. border-color: rgba(255, 255, 255, 0.5); /* Bordure plus marquée */
  1169. opacity: 0.7; /* Légère transparence pour indiquer une action */
  1170. }
  1171. `;
  1172. document.head.appendChild(style); // Ajout de la balise style dans le <head> du document
  1173. }
  1174.  
  1175. function addScrollBarStyles() {
  1176. // Créer une feuille de style pour personnaliser la scrollbar
  1177. const style = document.createElement('style');
  1178. style.textContent = `
  1179. .thumbnail-scroll-container::-webkit-scrollbar {
  1180. height: 6px;
  1181. }
  1182.  
  1183. .thumbnail-scroll-container::-webkit-scrollbar-track {
  1184. background: transparent;
  1185. }
  1186.  
  1187. /* Barre de défilement (thumb) légèrement plus visible */
  1188. .thumbnail-scroll-container::-webkit-scrollbar-thumb {
  1189. background-color: rgba(255, 255, 255, 0.15);
  1190. border-radius: 10px;
  1191. border: 2px solid transparent;
  1192. background-clip: padding-box;
  1193. }
  1194.  
  1195. .thumbnail-scroll-container::-webkit-scrollbar-thumb:hover {
  1196. background-color: rgba(255, 255, 255, 0.25);
  1197. transition: background-color 0.3s ease;
  1198. }
  1199.  
  1200. .thumbnail-scroll-container::-webkit-scrollbar-thumb:active {
  1201. background-color: rgba(255, 255, 255, 0.35);
  1202. }
  1203.  
  1204. .thumbnail-scroll-container {
  1205. scrollbar-width: thin;
  1206. scrollbar-color: rgba(255, 255, 255, 0.15) transparent;
  1207. }
  1208.  
  1209. .thumbnail-scroll-container:hover {
  1210. scrollbar-color: rgba(255, 255, 255, 0.25) transparent;
  1211. }
  1212. `;
  1213. // Ajouter le style au document
  1214. document.head.appendChild(style);
  1215. }
  1216.  
  1217. function injectStyles(){
  1218. addSpinnerStyles();
  1219. addScrollBarStyles();
  1220. addDownloadButtonStyles();
  1221. }
  1222.  
  1223. const parentClasses = '.txt-msg, .message, .conteneur-message.mb-3, .bloc-editor-forum, .signature-msg, .previsu-editor';
  1224. const linkSelectors = parentClasses.split(', ').map(cls => `${cls} a`);
  1225.  
  1226. // Ajouter des écouteurs d'événements aux images sur la page
  1227. function addListeners() {
  1228. linkSelectors.forEach(selector => {
  1229. document.querySelectorAll(selector).forEach(link => {
  1230. link.addEventListener('click', handleImageClick, true);
  1231. });
  1232. });
  1233. }
  1234.  
  1235. function handleImageClick(event) {
  1236. // Si Ctrl ou Cmd est enfoncé, ne pas ouvrir l'ImageViewer
  1237. if (event.ctrlKey || event.metaKey) {
  1238. return;
  1239. }
  1240.  
  1241. const imgElement = this.querySelector('img');
  1242. if (imgElement) {
  1243. event.preventDefault();
  1244. const closestElement = this.closest(parentClasses);
  1245. if (closestElement) {
  1246. const images = Array.from(closestElement.querySelectorAll('a')).filter(imgLink => imgLink.querySelector('img'));
  1247. const currentIndex = images.indexOf(this);
  1248.  
  1249. const viewer = new ImageViewer();
  1250. viewer.openViewer(images, currentIndex);
  1251. }
  1252. }
  1253. }
  1254.  
  1255. function isMobile() {
  1256. return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
  1257. }
  1258.  
  1259. function getImageExtension(url) {
  1260. const match = url.match(/\.(jpg|jpeg|png|gif|bmp|webp|tiff)$/i); // Regexp pour matcher les extensions d'images
  1261. return match ? match[0].toLowerCase() : null;
  1262. }
  1263.  
  1264. // Observer les changements dans le DOM
  1265. function observeDOMChanges() {
  1266. const observer = new MutationObserver(() => addListeners());
  1267. observer.observe(document, { childList: true, subtree: true });
  1268. }
  1269.  
  1270. // Détection des changements d'URL
  1271. function observeURLChanges() {
  1272. let lastUrl = window.location.href;
  1273.  
  1274. const urlObserver = new MutationObserver(() => {
  1275. if (lastUrl !== window.location.href) {
  1276. lastUrl = window.location.href;
  1277. addListeners();
  1278. }
  1279. });
  1280. urlObserver.observe(document, { subtree: true, childList: true });
  1281. }
  1282.  
  1283. function main() {
  1284. injectStyles();
  1285. addListeners();
  1286. observeDOMChanges();
  1287. observeURLChanges();
  1288. }
  1289.  
  1290. main();
  1291.  
  1292. })();