Enhanced 8chan UI

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

目前为 2025-04-19 提交的版本,查看 最新版本

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