Enhanced 8chan UI

Creates a media gallery with blur toggle and live thread info (Posts, Users, Files) plus additional enhancements

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

  1. // ==UserScript==
  2. // @name Enhanced 8chan UI
  3. // @version 1.6.7
  4. // @description Creates a media gallery with blur toggle and live thread info (Posts, Users, Files) plus additional enhancements
  5. // @match https://8chan.moe/*/res/*
  6. // @match https://8chan.se/*/res/*
  7. // @grant GM_addStyle
  8. // @grant GM.addStyle
  9. // @license MIT
  10. // @namespace https://greasyfork.org/users/1459581
  11. // ==/UserScript==
  12.  
  13.  
  14. (function () {
  15. 'use strict';
  16.  
  17. // Check if we're on a thread page
  18. const isThreadPage = window.location.href.match(/https:\/\/8chan\.moe\/.*\/res\/.*/);
  19.  
  20. // Default configuration for additional features
  21. var defaultConfig = {}; // TODO add menu and default configs to toggle options
  22.  
  23. // Main gallery functionality
  24. let currentIndex = 0;
  25. const mediaElements = [];
  26.  
  27. GM_addStyle(`
  28. .postCell {
  29. margin: 0 !important;
  30. }
  31. #navBoardsSpan {
  32. font-size: large;
  33. }
  34. .gallery-button {
  35. position: fixed;
  36. right: 20px;
  37. z-index: 9999;
  38. background: #333;
  39. color: white;
  40. padding: 15px;
  41. border-radius: 50%;
  42. cursor: pointer;
  43. box-shadow: 0 2px 5px rgba(0,0,0,0.3);
  44. text-align: center;
  45. line-height: 1;
  46. font-size: 20px;
  47. }
  48. .gallery-button.blur-toggle {
  49. bottom: 80px;
  50. }
  51. .gallery-button.gallery-open {
  52. bottom: 20px;
  53. }
  54. #media-count-display {
  55. position: fixed;
  56. bottom: 150px;
  57. right: 20px;
  58. background: #444;
  59. color: white;
  60. padding: 8px 12px;
  61. border-radius: 10px;
  62. font-size: 14px;
  63. z-index: 9999;
  64. box-shadow: 0 2px 5px rgba(0,0,0,0.3);
  65. white-space: nowrap;
  66. }
  67. .gallery-modal {
  68. display: none;
  69. position: fixed;
  70. bottom: 80px;
  71. right: 20px;
  72. width: 80%;
  73. max-width: 600px;
  74. max-height: 80vh;
  75. background: oklch(21% 0.006 285.885);
  76. border-radius: 10px;
  77. padding: 20px;
  78. overflow-y: auto;
  79. z-index: 9998;
  80. }
  81. .gallery-grid {
  82. display: grid;
  83. grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
  84. gap: 10px;
  85. }
  86. .media-item {
  87. position: relative;
  88. cursor: pointer;
  89. aspect-ratio: 1;
  90. overflow: hidden;
  91. border-radius: 5px;
  92. }
  93. .media-thumbnail {
  94. width: 100%;
  95. height: 100%;
  96. object-fit: cover;
  97. }
  98. .media-type-icon {
  99. position: absolute;
  100. bottom: 5px;
  101. right: 5px;
  102. color: white;
  103. background: rgba(0,0,0,0.5);
  104. padding: 2px 5px;
  105. border-radius: 3px;
  106. font-size: 0.8em;
  107. }
  108. .lightbox {
  109. display: none;
  110. position: fixed;
  111. top: 0;
  112. left: 0;
  113. width: 100%;
  114. height: 100%;
  115. background: rgba(0,0,0,0.9);
  116. z-index: 10000;
  117. }
  118. .lightbox-content {
  119. position: absolute;
  120. top: 45%;
  121. left: 50%;
  122. transform: translate(-50%, -50%);
  123. max-width: 90%;
  124. max-height: 90%;
  125. }
  126. .lightbox-video {
  127. max-width: 90vw;
  128. max-height: 90vh;
  129. }
  130. .close-btn {
  131. position: absolute;
  132. top: 20px;
  133. right: 20px;
  134. width: 50px;
  135. height: 50px;
  136. cursor: pointer;
  137. }
  138. .lightbox-nav {
  139. position: absolute;
  140. top: 50%;
  141. transform: translateY(-50%);
  142. background: rgba(255,255,255,0.2);
  143. color: white;
  144. border: none;
  145. padding: 15px;
  146. cursor: pointer;
  147. font-size: 24px;
  148. border-radius: 50%;
  149. }
  150. .lightbox-prev {
  151. left: 20px;
  152. }
  153. .lightbox-next {
  154. right: 20px;
  155. }
  156. .go-to-post-btn {
  157. position: absolute;
  158. bottom: 10px;
  159. left: 50%;
  160. transform: translateX(-50%);
  161. background: rgba(255,255,255,0.1);
  162. color: white;
  163. border: none;
  164. padding: 8px 15px;
  165. border-radius: 20px;
  166. cursor: pointer;
  167. font-size: 14px;
  168. }
  169. .blurred-media img,
  170. .blurred-media video,
  171. .blurred-media audio {
  172. filter: blur(10px) brightness(0.8);
  173. transition: filter 0.3s ease;
  174. }
  175.  
  176. /* New styles for centered quick-reply */
  177. #quick-reply.centered {
  178. position: fixed;
  179. top: 50% !important;
  180. left: 50% !important;
  181. transform: translate(-50%, -50%);
  182. width: 80%;
  183. max-width: 800px;
  184. min-height: 550px;
  185. background: oklch(21% 0.006 285.885);
  186. padding: 10px !important;
  187. border-radius: 10px;
  188. z-index: 9999;
  189. box-shadow: 0 0 20px rgba(0,0,0,0.5);
  190. }
  191. #quick-reply table,
  192. #quick-reply.centered #qrname,
  193. #quick-reply.centered #qrsubject,
  194. #quick-reply.centered #qrbody {
  195. width: 100% !important;
  196. max-width: 100% !important;
  197. box-sizing: border-box;
  198. }
  199. #quick-reply.centered #qrbody {
  200. min-height: 200px;
  201. }
  202. #quick-reply-overlay {
  203. position: fixed;
  204. top: 0;
  205. left: 0;
  206. width: 100%;
  207. height: 100%;
  208. background: rgba(0,0,0,0.7);
  209. z-index: 99;
  210. display: none;
  211. }
  212.  
  213.  
  214. /* Cleanup */
  215. #footer,
  216. #postingForm,
  217. #actionsForm,
  218. #navTopBoardsSpan,
  219. .coloredIcon.linkOverboard,
  220. .coloredIcon.linkSfwOver,
  221. .coloredIcon.multiboardButton,
  222. #navLinkSpan>span:nth-child(9),
  223. #navLinkSpan>span:nth-child(11),
  224. #navLinkSpan>span:nth-child(13) {
  225. display: none;
  226. }
  227. /* Header */
  228. #dynamicHeaderThread,
  229. .navHeader {
  230. box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
  231. }
  232. /* Thread Watcher */
  233. #watchedMenu .floatingContainer {
  234. min-width: 330px;
  235. }
  236. #watchedMenu .watchedCellLabel > a:after {
  237. content: " - "attr(href);
  238. filter: saturate(50%);
  239. font-style: italic;
  240. font-weight: bold;
  241. }
  242. #watchedMenu {
  243. box-shadow: -3px 3px 2px 0px rgba(0,0,0,0.19);
  244. }
  245. /* Posts */
  246. .quoteTooltip .innerPost {
  247. overflow: hidden;
  248. box-shadow: -3px 3px 2px 0px rgba(0,0,0,0.19);
  249. }
  250.  
  251. /* Catalog page CSS */
  252. #dynamicAnnouncement {
  253. display: none;
  254. }
  255. #postingForm {
  256. margin: 2em auto;
  257. }
  258. `);
  259.  
  260. // Only create thread-specific UI elements if we're on a thread page
  261. if (isThreadPage) {
  262. // Create gallery UI elements
  263. const galleryButton = document.createElement('div');
  264. galleryButton.className = 'gallery-button gallery-open';
  265. galleryButton.textContent = '🎴';
  266. galleryButton.title = 'Gallery';
  267. document.body.appendChild(galleryButton);
  268.  
  269. const blurToggle = document.createElement('div');
  270. blurToggle.className = 'gallery-button blur-toggle';
  271. blurToggle.textContent = '💼';
  272. blurToggle.title = 'Goon Mode';
  273. document.body.appendChild(blurToggle);
  274.  
  275. const replyButton = document.createElement('div');
  276. replyButton.id = 'replyButton';
  277. replyButton.className = 'gallery-button';
  278. replyButton.style.bottom = '190px';
  279. replyButton.textContent = '✏️';
  280. replyButton.title = 'Reply';
  281. document.body.appendChild(replyButton);
  282.  
  283. const mediaInfoDisplay = document.createElement('div');
  284. mediaInfoDisplay.id = 'media-count-display';
  285. document.body.appendChild(mediaInfoDisplay);
  286.  
  287. // Create overlay for quick-reply
  288. const overlay = document.createElement('div');
  289. overlay.id = 'quick-reply-overlay';
  290. document.body.appendChild(overlay);
  291.  
  292. let isBlurred = false;
  293.  
  294. blurToggle.addEventListener('click', () => {
  295. isBlurred = !isBlurred;
  296. blurToggle.textContent = isBlurred ? '🍆' : '💼';
  297. blurToggle.title = isBlurred ? 'SafeMode' : 'Goon Mode';
  298. document.querySelectorAll('div.innerPost').forEach(post => {
  299. post.classList.toggle('blurred-media', isBlurred);
  300. });
  301. });
  302.  
  303. function setupQuickReply() {
  304. const quickReply = document.getElementById('quick-reply');
  305. if (!quickReply) return;
  306.  
  307. // Create close button if it doesn't exist
  308. if (!quickReply.querySelector('.qr-close-btn')) {
  309. const closeBtn = document.createElement('div');
  310. closeBtn.className = 'close-btn qr-close-btn';
  311. closeBtn.textContent = ' ';
  312. closeBtn.style.position = 'absolute';
  313. closeBtn.style.top = '10px';
  314. closeBtn.style.right = '10px';
  315. closeBtn.style.cursor = 'pointer';
  316. closeBtn.addEventListener('click', () => {
  317. quickReply.classList.remove('centered');
  318. overlay.style.display = 'none';
  319. });
  320. quickReply.appendChild(closeBtn);
  321. }
  322.  
  323. quickReply.classList.add('centered');
  324. overlay.style.display = 'block';
  325.  
  326. // Focus on reply body
  327. setTimeout(() => {
  328. document.querySelector('#qrbody')?.focus();
  329. }, 100);
  330. }
  331.  
  332. replyButton.addEventListener('click', () => {
  333. const nativeReplyBtn = document.querySelector('a#replyButton[href="#postingForm"]');
  334. if (nativeReplyBtn) {
  335. nativeReplyBtn.click();
  336. } else {
  337. location.hash = '#postingForm';
  338. }
  339.  
  340. // Clear form fields and setup centered quick-reply
  341. setTimeout(() => {
  342. document.querySelectorAll('#qrname, #qrsubject, #qrbody').forEach(field => {
  343. field.value = '';
  344. });
  345. setupQuickReply();
  346. }, 100);
  347. });
  348.  
  349. const galleryModal = document.createElement('div');
  350. galleryModal.className = 'gallery-modal';
  351. const galleryGrid = document.createElement('div');
  352. galleryGrid.className = 'gallery-grid';
  353. galleryModal.appendChild(galleryGrid);
  354. document.body.appendChild(galleryModal);
  355.  
  356. const lightbox = document.createElement('div');
  357. lightbox.className = 'lightbox';
  358. lightbox.innerHTML = `
  359. <div class="close-btn">×</div>
  360. <button class="lightbox-nav lightbox-prev">←</button>
  361. <button class="lightbox-nav lightbox-next">→</button>
  362. `;
  363. document.body.appendChild(lightbox);
  364.  
  365. function collectMedia() {
  366. mediaElements.length = 0;
  367. const seenUrls = new Set();
  368. document.querySelectorAll('div.innerPost').forEach(post => {
  369. post.querySelectorAll('img[loading="lazy"]').forEach(img => {
  370. const src = img.src;
  371. if (!src || seenUrls.has(src)) return;
  372. const parentLink = img.closest('a');
  373. const href = parentLink?.href;
  374. if (href && !seenUrls.has(href)) {
  375. seenUrls.add(href);
  376. mediaElements.push({
  377. element: parentLink,
  378. thumbnail: img,
  379. url: href,
  380. type: /\.(mp4|webm|mov)$/i.test(href) ? 'VIDEO' :
  381. /\.(mp3|wav|ogg)$/i.test(href) ? 'AUDIO' : 'IMAGE',
  382. postElement: post
  383. });
  384. } else {
  385. seenUrls.add(src);
  386. mediaElements.push({
  387. element: img,
  388. thumbnail: img,
  389. url: src,
  390. type: 'IMAGE',
  391. postElement: post
  392. });
  393. }
  394. });
  395.  
  396. post.querySelectorAll('a[href*=".media"]:not(:has(img)), a.imgLink:not(:has(img))').forEach(link => {
  397. const href = link.href;
  398. if (!href || seenUrls.has(href)) return;
  399. const ext = href.split('.').pop().toLowerCase();
  400. if (/\.(jpg|jpeg|png|gif|webp|mp4|webm|mov|mp3|wav|ogg)$/i.test(ext)) {
  401. seenUrls.add(href);
  402. mediaElements.push({
  403. element: link,
  404. thumbnail: null,
  405. url: href,
  406. type: /\.(mp4|webm|mov)$/i.test(ext) ? 'VIDEO' :
  407. /\.(mp3|wav|ogg)$/i.test(ext) ? 'AUDIO' : 'IMAGE',
  408. postElement: post
  409. });
  410. }
  411. });
  412. });
  413. }
  414.  
  415. function createGalleryItems() {
  416. galleryGrid.innerHTML = '';
  417. mediaElements.forEach((media, index) => {
  418. const item = document.createElement('div');
  419. item.className = 'media-item';
  420. const thumbnail = document.createElement('img');
  421. thumbnail.className = 'media-thumbnail';
  422. thumbnail.loading = 'lazy';
  423. thumbnail.src = media.thumbnail?.src || (
  424. media.type === 'VIDEO' ? 'https://via.placeholder.com/100/333/fff?text=VID' :
  425. media.type === 'AUDIO' ? 'https://via.placeholder.com/100/333/fff?text=AUD' :
  426. media.url
  427. );
  428. const typeIcon = document.createElement('div');
  429. typeIcon.className = 'media-type-icon';
  430. typeIcon.textContent = media.type === 'VIDEO' ? 'VID' :
  431. media.type === 'AUDIO' ? 'AUD' : 'IMG';
  432. item.appendChild(thumbnail);
  433. item.appendChild(typeIcon);
  434. item.addEventListener('click', () => showLightbox(media, index));
  435. galleryGrid.appendChild(item);
  436. });
  437. }
  438.  
  439. function showLightbox(media, index) {
  440. currentIndex = typeof index === 'number' ? index : mediaElements.indexOf(media);
  441. updateLightboxContent();
  442. lightbox.style.display = 'block';
  443. }
  444.  
  445. function updateLightboxContent() {
  446. const media = mediaElements[currentIndex];
  447. let content;
  448. if (media.type === 'AUDIO') {
  449. content = document.createElement('audio');
  450. content.controls = true;
  451. content.className = 'lightbox-content';
  452. content.src = media.url;
  453. } else if (media.type === 'VIDEO') {
  454. content = document.createElement('video');
  455. content.controls = true;
  456. content.className = 'lightbox-content lightbox-video';
  457. content.src = media.url;
  458. content.autoplay = true;
  459. content.loop = true;
  460. } else {
  461. content = document.createElement('img');
  462. content.className = 'lightbox-content';
  463. content.src = media.url;
  464. content.loading = 'eager';
  465. }
  466.  
  467. lightbox.querySelector('.lightbox-content')?.remove();
  468. lightbox.querySelector('.go-to-post-btn')?.remove();
  469.  
  470. const goToPostBtn = document.createElement('button');
  471. goToPostBtn.className = 'go-to-post-btn';
  472. goToPostBtn.textContent = 'Go to post';
  473. goToPostBtn.addEventListener('click', () => {
  474. lightbox.style.display = 'none';
  475. media.postElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
  476. media.postElement.style.transition = 'box-shadow 0.5s ease';
  477. media.postElement.style.boxShadow = '0 0 0 3px rgba(255, 255, 0, 0.5)';
  478. setTimeout(() => {
  479. media.postElement.style.boxShadow = 'none';
  480. }, 2000);
  481. });
  482.  
  483. lightbox.appendChild(content);
  484. lightbox.appendChild(goToPostBtn);
  485. }
  486.  
  487. function navigate(direction) {
  488. currentIndex = (currentIndex + direction + mediaElements.length) % mediaElements.length;
  489. updateLightboxContent();
  490. }
  491.  
  492. function updateThreadInfoDisplay() {
  493. const postCount = document.getElementById('postCount')?.textContent || '0';
  494. const userCount = document.getElementById('userCountLabel')?.textContent || '0';
  495. const fileCount = document.getElementById('fileCount')?.textContent || '0';
  496. mediaInfoDisplay.textContent = `Posts: ${postCount} | Users: ${userCount} | Files: ${fileCount}`;
  497. }
  498.  
  499. lightbox.querySelector('.lightbox-prev').addEventListener('click', () => navigate(-1));
  500. lightbox.querySelector('.lightbox-next').addEventListener('click', () => navigate(1));
  501. lightbox.querySelector('.close-btn').addEventListener('click', () => {
  502. lightbox.style.display = 'none';
  503. });
  504.  
  505. galleryButton.addEventListener('click', () => {
  506. collectMedia();
  507. createGalleryItems();
  508. galleryModal.style.display = galleryModal.style.display === 'block' ? 'none' : 'block';
  509. });
  510.  
  511. document.addEventListener('click', (e) => {
  512. if (!galleryModal.contains(e.target) && !galleryButton.contains(e.target)) {
  513. galleryModal.style.display = 'none';
  514. }
  515. });
  516.  
  517. document.addEventListener('keydown', (e) => {
  518. if (lightbox.style.display === 'block') {
  519. if (e.key === 'ArrowLeft') navigate(-1);
  520. if (e.key === 'ArrowRight') navigate(1);
  521. }
  522.  
  523. if (e.key === 'Escape') {
  524. galleryModal.style.display = 'none';
  525. lightbox.style.display = 'none';
  526.  
  527. const qrCloseBtn = document.querySelector('.quick-reply .close-btn, th .close-btn');
  528. if (qrCloseBtn && typeof qrCloseBtn.click === 'function') {
  529. qrCloseBtn.click();
  530. }
  531.  
  532. const qrFields = document.querySelectorAll('#qrname, #qrsubject, #qrbody');
  533. qrFields.forEach(field => {
  534. field.value = '';
  535. });
  536.  
  537. // Also hide overlay and centered quick-reply
  538. document.getElementById('quick-reply-overlay').style.display = 'none';
  539. document.getElementById('quick-reply')?.classList.remove('centered');
  540. }
  541.  
  542. if (e.altKey && e.key.toLowerCase() === 'z') {
  543. replyButton.click();
  544. }
  545. });
  546.  
  547. // Initialize main gallery functionality
  548. collectMedia();
  549. createGalleryItems();
  550. updateThreadInfoDisplay();
  551. setInterval(updateThreadInfoDisplay, 5000);
  552. }
  553.  
  554. // The following features are available on all pages
  555. // Header Catalog Links
  556. // Function to append /catalog.html to links
  557. function appendCatalogToLinks() {
  558. const navboardsSpan = document.getElementById('navBoardsSpan');
  559. if (navboardsSpan) {
  560. const links = navboardsSpan.getElementsByTagName('a');
  561. for (let link of links) {
  562. if (link.href && !link.href.endsWith('/catalog.html')) {
  563. link.href += '/catalog.html';
  564. }
  565. }
  566. }
  567. }
  568. // Initial call to append links on page load
  569. appendCatalogToLinks();
  570.  
  571. // Set up a MutationObserver to watch for changes in the #navboardsSpan div
  572. const observer = new MutationObserver(appendCatalogToLinks);
  573. const config = { childList: true, subtree: true };
  574.  
  575. const navboardsSpan = document.getElementById('navBoardsSpan');
  576. if (navboardsSpan) {
  577. observer.observe(navboardsSpan, config);
  578. }
  579.  
  580. // Scroll to last read post
  581. // Function to save the scroll position
  582. const MAX_PAGES = 50; // Maximum number of pages to store scroll positions
  583. const currentPage = window.location.href;
  584.  
  585. // Specify pages to exclude from scroll position saving (supports wildcards)
  586. const excludedPagePatterns = [
  587. /\/catalog\.html$/i, // Exclude any page ending with /catalog.html (case-insensitive)
  588. // Add more patterns as needed
  589. ];
  590.  
  591. // Function to check if current page matches any exclusion pattern
  592. function isExcludedPage(url) {
  593. return excludedPagePatterns.some(pattern => pattern.test(url));
  594. }
  595.  
  596. // Function to save the scroll position for the current page
  597. function saveScrollPosition() {
  598. // Check if the current page matches any excluded pattern
  599. if (isExcludedPage(currentPage)) {
  600. return; // Skip saving scroll position for excluded pages
  601. }
  602.  
  603. const scrollPosition = window.scrollY; // Get the current vertical scroll position
  604. localStorage.setItem(`scrollPosition_${currentPage}`, scrollPosition); // Store it in localStorage with a unique key
  605.  
  606. // Manage the number of stored scroll positions
  607. manageScrollStorage();
  608. }
  609.  
  610. // Function to restore the scroll position for the current page
  611. function restoreScrollPosition() {
  612. const savedPosition = localStorage.getItem(`scrollPosition_${currentPage}`); // Retrieve the saved position for the current page
  613. if (savedPosition) {
  614. window.scrollTo(0, parseInt(savedPosition, 10)); // Scroll to the saved position
  615. }
  616. }
  617.  
  618. // Function to manage the number of stored scroll positions
  619. function manageScrollStorage() {
  620. const keys = Object.keys(localStorage).filter(key => key.startsWith('scrollPosition_'));
  621.  
  622. // If the number of stored positions exceeds the limit, remove the oldest
  623. if (keys.length > MAX_PAGES) {
  624. // Sort keys by their creation time (assuming the order of keys reflects the order of storage)
  625. keys.sort((a, b) => {
  626. return localStorage.getItem(a) - localStorage.getItem(b);
  627. });
  628. // Remove the oldest entries until we are within the limit
  629. while (keys.length > MAX_PAGES) {
  630. localStorage.removeItem(keys.shift());
  631. }
  632. }
  633. }
  634.  
  635. // Event listener to save scroll position before the page unloads
  636. window.addEventListener('beforeunload', saveScrollPosition);
  637.  
  638. // Restore scroll position when the page loads
  639. window.addEventListener('load', restoreScrollPosition);
  640.  
  641. // Fix for Image Hover
  642. (function () {
  643. 'use strict';
  644.  
  645. // Function to handle mouse movement
  646. function onMouseMove(event) {
  647. const img = document.querySelector('img[style*="position: fixed"]');
  648. if (img) {
  649. // Get the viewport dimensions
  650. const viewportWidth = window.innerWidth;
  651. const viewportHeight = window.innerHeight;
  652.  
  653. // Calculate the new position
  654. let newX = event.clientX + 10; // Offset to avoid cursor overlap
  655. let newY = event.clientY + 10; // Offset to avoid cursor overlap
  656.  
  657. // Ensure the image stays within the viewport
  658. if (newX + img.width > viewportWidth) {
  659. newX = viewportWidth - img.width - 10; // Adjust for right edge
  660. }
  661. if (newY + img.height > viewportHeight) {
  662. newY = viewportHeight - img.height - 10; // Adjust for bottom edge
  663. }
  664.  
  665. // Update the image position
  666. img.style.left = `${newX}px`;
  667. img.style.top = `${newY}px`;
  668. }
  669. }
  670.  
  671. // Function to handle mouse enter and leave
  672. function onMouseEnter() {
  673. document.addEventListener('mousemove', onMouseMove);
  674. }
  675.  
  676. function onMouseLeave() {
  677. document.removeEventListener('mousemove', onMouseMove);
  678. }
  679.  
  680. // Observe for the image to appear and disappear
  681. const observer = new MutationObserver((mutations) => {
  682. mutations.forEach((mutation) => {
  683. mutation.addedNodes.forEach((node) => {
  684. if (node.nodeType === Node.ELEMENT_NODE && node.matches('img[style*="position: fixed"]')) {
  685. onMouseEnter();
  686. }
  687. });
  688. mutation.removedNodes.forEach((node) => {
  689. if (node.nodeType === Node.ELEMENT_NODE && node.matches('img[style*="position: fixed"]')) {
  690. onMouseLeave();
  691. }
  692. });
  693. });
  694. });
  695.  
  696. // Start observing the body for changes
  697. observer.observe(document.body, { childList: true, subtree: true });
  698. })();
  699. })();