4chan Gallery

4chan grid-based image gallery with zoom mode support for threads that allows you to browse images, and soundposts (images with sounds, webms with sounds) along with other utility features.

目前为 2024-07-26 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name 4chan Gallery
  3. // @namespace http://tampermonkey.net/
  4. // @version 2024-07-26 (2.6)
  5. // @description 4chan grid-based image gallery with zoom mode support for threads that allows you to browse images, and soundposts (images with sounds, webms with sounds) along with other utility features.
  6. // @author TheDarkEnjoyer
  7. // @match https://boards.4chan.org/*/thread/*
  8. // @match https://boards.4chan.org/*/archive
  9. // @match https://boards.4channel.org/*/thread/*
  10. // @match https://boards.4channel.org/*/archive
  11. // @match https://warosu.org/*/thread/*
  12. // @match https://warosu.org/*/
  13. // @match https://archived.moe/*/thread/*
  14. // @match https://archived.moe/*/
  15. // @match https://archive.palanq.win/*/
  16. // @match https://archive.palanq.win/*/thread/*
  17. // @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
  18. // @grant none
  19. // @license GNU GPLv3
  20. // ==/UserScript==
  21.  
  22. (function () {
  23. "use strict";
  24. // injectVideoJS();
  25. const defaultSettings = {
  26. Load_High_Res_Images_By_Default: {
  27. value: false,
  28. info: "When opening the gallery, load high quality images by default (no thumbnails)",
  29. },
  30. };
  31.  
  32. let threadURL = window.location.href;
  33. let lastScrollPosition = 0;
  34. let gallerySize = { width: 0, height: 0 };
  35.  
  36. // store settings in local storage
  37. if (!localStorage.getItem("gallerySettings")) {
  38. localStorage.setItem("gallerySettings", JSON.stringify(defaultSettings));
  39. }
  40. let settings = JSON.parse(localStorage.getItem("gallerySettings"));
  41.  
  42. function setStyles(element, styles) {
  43. for (const property in styles) {
  44. element.style[property] = styles[property];
  45. }
  46. }
  47.  
  48. function getPosts(websiteUrl, doc) {
  49. switch (websiteUrl) {
  50. case "warosu.org":
  51. return doc.querySelectorAll(".comment");
  52. case "archived.moe":
  53. case "archive.palanq.win":
  54. return doc.querySelectorAll(".has_image");
  55. case "boards.4chan.org":
  56. case "boards.4channel.org":
  57. default:
  58. return doc.querySelectorAll(".postContainer");
  59. }
  60. }
  61.  
  62. function getDocument(thread, threadURL) {
  63. return new Promise((resolve, reject) => {
  64. if (thread === threadURL) {
  65. resolve(document);
  66. } else {
  67. fetch(thread)
  68. .then((response) => response.text())
  69. .then((html) => {
  70. const parser = new DOMParser();
  71. const doc = parser.parseFromString(html, "text/html");
  72. resolve(doc);
  73. })
  74. .catch((error) => {
  75. reject(error);
  76. });
  77. }
  78. });
  79. }
  80.  
  81. function injectVideoJS() {
  82. const link = document.createElement("link");
  83. link.href = "https://vjs.zencdn.net/8.10.0/video-js.css";
  84. link.rel = "stylesheet";
  85. document.head.appendChild(link);
  86.  
  87. // theme
  88. const theme = document.createElement("link");
  89. theme.href = "https://unpkg.com/@videojs/themes@1/dist/city/index.css";
  90. theme.rel = "stylesheet";
  91. document.head.appendChild(theme);
  92.  
  93. const script = document.createElement("script");
  94. script.src = "https://vjs.zencdn.net/8.10.0/video.min.js";
  95. document.body.appendChild(script);
  96. ("VideoJS injected successfully!");
  97. }
  98.  
  99. const loadButton = () => {
  100. const isArchivePage = window.location.pathname.includes("/archive");
  101.  
  102. const button = document.createElement("button");
  103. button.textContent = "Open Image Gallery";
  104. button.id = "openImageGallery";
  105. setStyles(button, {
  106. position: "fixed",
  107. bottom: "20px",
  108. right: "20px",
  109. zIndex: "1000",
  110. backgroundColor: "#1c1c1c",
  111. color: "#d9d9d9",
  112. padding: "10px 20px",
  113. borderRadius: "5px",
  114. border: "none",
  115. cursor: "pointer",
  116. boxShadow: "0 2px 4px rgba(0, 0, 0, 0.3)",
  117. });
  118.  
  119. const openImageGallery = () => {
  120. const gallery = document.createElement("div");
  121. gallery.id = "imageGallery";
  122. setStyles(gallery, {
  123. position: "fixed",
  124. top: "0",
  125. left: "0",
  126. width: "100%",
  127. height: "100%",
  128. backgroundColor: "rgba(0, 0, 0, 0.8)",
  129. display: "flex",
  130. justifyContent: "center",
  131. alignItems: "center",
  132. zIndex: "9999",
  133. });
  134.  
  135. const gridContainer = document.createElement("div");
  136. setStyles(gridContainer, {
  137. display: "grid",
  138. gridTemplateColumns: `repeat(3, 1fr)`,
  139. gridTemplateRows: `repeat(2, 1fr)`,
  140. gap: "10px",
  141. padding: "20px",
  142. backgroundColor: "#1c1c1c",
  143. color: "#d9d9d9",
  144. maxWidth: "80%",
  145. maxHeight: "80%",
  146. overflowY: "auto",
  147. resize: "both",
  148. overflow: "auto",
  149. border: "1px solid #d9d9d9",
  150. });
  151.  
  152. // Restore the previous grid container size
  153. if (gallerySize.width > 0 && gallerySize.height > 0) {
  154. gridContainer.style.width = `${gallerySize.width}px`;
  155. gridContainer.style.height = `${gallerySize.height}px`;
  156. }
  157.  
  158. let mode = "all"; // Default mode is "all"
  159. let autoPlayWebms = false; // Default auto play webms without sound is false
  160.  
  161. // top left corner of the screen
  162. const mediaTypeButtonContainer = document.createElement("div");
  163. setStyles(mediaTypeButtonContainer, {
  164. position: "absolute",
  165. top: "10px",
  166. left: "10px",
  167. display: "flex",
  168. gap: "10px",
  169. });
  170.  
  171. // Toggle mode button
  172. const toggleModeButton = document.createElement("button");
  173. toggleModeButton.textContent = "Toggle Mode (All)";
  174. setStyles(toggleModeButton, {
  175. backgroundColor: "#1c1c1c",
  176. color: "#d9d9d9",
  177. padding: "10px 20px",
  178. borderRadius: "5px",
  179. border: "none",
  180. cursor: "pointer",
  181. boxShadow: "0 2px 4px rgba(0, 0, 0, 0.3)",
  182. });
  183. toggleModeButton.addEventListener("click", () => {
  184. mode = mode === "all" ? "webm" : "all";
  185. toggleModeButton.textContent = `Toggle Mode (${mode === "all" ? "All" : "Webm & Images with Sound"
  186. })`;
  187. gridContainer.innerHTML = ""; // Clear the grid
  188. loadPosts(mode); // Reload posts based on the new mode
  189. });
  190.  
  191. // Toggle auto play webms button
  192. const toggleAutoPlayButton = document.createElement("button");
  193. toggleAutoPlayButton.textContent = "Auto Play Webms without Sound";
  194. setStyles(toggleAutoPlayButton, {
  195. backgroundColor: "#1c1c1c",
  196. color: "#d9d9d9",
  197. padding: "10px 20px",
  198. borderRadius: "5px",
  199. border: "none",
  200. cursor: "pointer",
  201. boxShadow: "0 2px 4px rgba(0, 0, 0, 0.3)",
  202. });
  203. toggleAutoPlayButton.addEventListener("click", () => {
  204. autoPlayWebms = !autoPlayWebms;
  205. toggleAutoPlayButton.textContent = autoPlayWebms
  206. ? "Stop Auto Play Webms"
  207. : "Auto Play Webms without Sound";
  208. gridContainer.innerHTML = ""; // Clear the grid
  209. loadPosts(mode); // Reload posts based on the new mode and auto play setting
  210. });
  211. mediaTypeButtonContainer.appendChild(toggleModeButton);
  212. mediaTypeButtonContainer.appendChild(toggleAutoPlayButton);
  213. gallery.appendChild(mediaTypeButtonContainer);
  214.  
  215. // settings button on the top right corner of the screen
  216. const settingsButton = document.createElement("button");
  217. settingsButton.id = "settingsButton";
  218. settingsButton.textContent = "Settings";
  219. setStyles(settingsButton, {
  220. position: "absolute",
  221. top: "20px",
  222. right: "20px",
  223. backgroundColor: "#007bff", // Primary color
  224. color: "#fff",
  225. padding: "10px 20px",
  226. borderRadius: "5px",
  227. border: "none",
  228. cursor: "pointer",
  229. boxShadow: "0 2px 4px rgba(0, 0, 0, 0.3)",
  230. transition: "background-color 0.3s ease",
  231. });
  232. settingsButton.addEventListener("click", () => {
  233. const settingsContainer = document.createElement("div");
  234. settingsContainer.id = "settingsContainer";
  235. setStyles(settingsContainer, {
  236. position: "fixed",
  237. top: "0",
  238. left: "0",
  239. width: "100%",
  240. height: "100%",
  241. backgroundColor: "rgba(0, 0, 0, 0.8)",
  242. display: "flex",
  243. justifyContent: "center",
  244. alignItems: "center",
  245. zIndex: "9999",
  246. animation: "fadeIn 0.3s ease",
  247. });
  248.  
  249. const settingsBox = document.createElement("div");
  250. setStyles(settingsBox, {
  251. backgroundColor: "#000000", // Background color
  252. color: "#ffffff", // Text color
  253. padding: "30px",
  254. borderRadius: "10px",
  255. border: "1px solid #6c757d", // Secondary color
  256. maxWidth: "80%",
  257. maxHeight: "80%",
  258. overflowY: "auto",
  259. boxShadow: "0 4px 6px rgba(0, 0, 0, 0.1)",
  260. });
  261.  
  262. const settingsTitle = document.createElement("h2");
  263. settingsTitle.id = "settingsTitle";
  264. settingsTitle.textContent = "Settings";
  265. setStyles(settingsTitle, {
  266. textAlign: "center",
  267. marginBottom: "20px",
  268. });
  269.  
  270. const settingsList = document.createElement("ul");
  271. settingsList.id = "settingsList";
  272. setStyles(settingsList, {
  273. listStyleType: "none",
  274. padding: "0",
  275. margin: "0",
  276. });
  277.  
  278. // include default settings as existing settings inside the input fields
  279. // have an icon next to the setting that explains what the setting does
  280. for (const setting in settings) {
  281. const settingItem = document.createElement("li");
  282. setStyles(settingItem, {
  283. display: "flex",
  284. alignItems: "center",
  285. marginBottom: "15px",
  286. });
  287.  
  288. const settingLabel = document.createElement("label");
  289. settingLabel.textContent = setting.replace(/_/g, " ");
  290. settingLabel.title = settings[setting].info;
  291. setStyles(settingLabel, {
  292. flex: "1",
  293. display: "flex",
  294. alignItems: "center",
  295. });
  296.  
  297. const settingIcon = document.createElement("span");
  298. settingIcon.className = "material-icons-outlined";
  299. settingIcon.textContent = settings[setting].icon;
  300. settingIcon.style.marginRight = "10px";
  301. settingLabel.prepend(settingIcon);
  302.  
  303. settingItem.appendChild(settingLabel);
  304.  
  305. const settingInput = document.createElement("input");
  306. const settingValueType = typeof defaultSettings[setting].value;
  307. if (settingValueType === "boolean") {
  308. settingInput.type = "checkbox";
  309. settingInput.checked = settings[setting].value;
  310. } else if (settingValueType === "number") {
  311. settingInput.type = "number";
  312. settingInput.value = settings[setting].value;
  313. } else {
  314. settingInput.type = "text";
  315. settingInput.value = settings[setting].value;
  316. }
  317. setStyles(settingInput, {
  318. padding: "8px 12px",
  319. borderRadius: "5px",
  320. border: "1px solid #6c757d", // Secondary color
  321. flex: "2",
  322. });
  323. settingInput.addEventListener("focus", () => {
  324. setStyles(settingInput, {
  325. borderColor: "#007bff", // Primary color
  326. boxShadow: "0 0 0 2px rgba(0, 123, 255, 0.25)",
  327. outline: "none",
  328. });
  329. });
  330. settingInput.addEventListener("blur", () => {
  331. setStyles(settingInput, {
  332. borderColor: "#6c757d", // Secondary color
  333. boxShadow: "none",
  334. });
  335. });
  336.  
  337. if (settingValueType === "boolean") {
  338. settingInput.style.marginRight = "10px";
  339. }
  340.  
  341. settingItem.appendChild(settingInput);
  342. settingsList.appendChild(settingItem);
  343. }
  344.  
  345. const saveButton = document.createElement("button");
  346. saveButton.id = "saveButton";
  347. saveButton.textContent = "Save";
  348. setStyles(saveButton, {
  349. backgroundColor: "#007bff", // Primary color
  350. color: "#fff",
  351. padding: "10px 20px",
  352. borderRadius: "5px",
  353. border: "none",
  354. cursor: "pointer",
  355. boxShadow: "0 2px 4px rgba(0, 0, 0, 0.3)",
  356. transition: "background-color 0.3s ease",
  357. marginRight: "10px",
  358. });
  359. saveButton.addEventListener("click", () => {
  360. const newSettings = defaultSettings;
  361. const inputs = document.querySelectorAll("#settingsList input");
  362. inputs.forEach((input) => {
  363. const settingName = input.previousSibling.textContent.replace(
  364. / /g,
  365. "_"
  366. );
  367. const settingValue =
  368. typeof defaultSettings[settingName].value === "boolean"
  369. ? input.checked
  370. : input.value;
  371. newSettings[settingName].value = settingValue;
  372. });
  373. localStorage.setItem("gallerySettings", JSON.stringify(newSettings));
  374. settings = newSettings;
  375. settingsContainer.remove();
  376. gridContainer.innerHTML = ""; // Clear the grid
  377. loadPosts(mode); // Reload posts based on the new settings
  378. });
  379.  
  380. // Close button
  381. const closeButton = document.createElement("button");
  382. closeButton.id = "closeButton";
  383. closeButton.textContent = "Close";
  384. setStyles(closeButton, {
  385. backgroundColor: "#007bff", // Primary color
  386. color: "#fff",
  387. padding: "10px 20px",
  388. borderRadius: "5px",
  389. border: "none",
  390. cursor: "pointer",
  391. boxShadow: "0 2px 4px rgba(0, 0, 0, 0.3)",
  392. transition: "background-color 0.3s ease",
  393. });
  394. closeButton.addEventListener("click", () => {
  395. settingsContainer.remove();
  396. });
  397.  
  398. settingsBox.appendChild(settingsTitle);
  399. settingsBox.appendChild(settingsList);
  400. settingsBox.appendChild(saveButton);
  401. settingsBox.appendChild(closeButton);
  402. settingsContainer.appendChild(settingsBox);
  403. gallery.appendChild(settingsContainer);
  404. });
  405.  
  406. // Hover effect for settings button
  407. settingsButton.addEventListener("mouseenter", () => {
  408. settingsButton.style.backgroundColor = "#0056b3";
  409. });
  410. settingsButton.addEventListener("mouseleave", () => {
  411. settingsButton.style.backgroundColor = "#007bff";
  412. });
  413.  
  414. gallery.appendChild(settingsButton);
  415.  
  416. const loadPosts = (mode) => {
  417. const checkedThreads = isArchivePage
  418. ? // Get all checked threads in the archive page or the current link if it's not an archive page
  419. Array.from(
  420. document.querySelectorAll(
  421. ".flashListing input[type='checkbox']:checked"
  422. )
  423. ).map((checkbox) => {
  424. let archiveSite =
  425. checkbox.parentNode.parentNode.querySelector("a").href;
  426. return archiveSite;
  427. })
  428. : [threadURL];
  429.  
  430. const loadPostsFromThread = (thread) => {
  431. // get the website url without the protocol and next slash
  432. const websiteUrl = thread.replace(/(^\w+:|^)\/\//, "").split("/")[0];
  433.  
  434. // const board = thread.split("/thread/")[0].split("/").pop();
  435. // const threadNo = `${parseInt(thread.split("thread/").pop())}`
  436. getDocument(thread, threadURL).then((doc) => {
  437. let posts;
  438.  
  439. // use a case statement to deal with different websites
  440. posts = getPosts(websiteUrl, doc);
  441.  
  442. posts.forEach((post) => {
  443. let mediaLinkFlag = false;
  444. let board;
  445. let threadID;
  446. let postID;
  447. let postURL;
  448. let thumbnailUrl;
  449. let mediaLink;
  450. let fileName;
  451. let comment;
  452.  
  453. let isVideo;
  454. let isImage;
  455. let soundLink;
  456. let encodedSoundPostLink;
  457. let temp;
  458.  
  459. // case statement for different websites
  460. switch (websiteUrl) {
  461. case "warosu.org":
  462. let thumbnailElement = post.querySelector("img");
  463.  
  464. fileName = post
  465. .querySelector(".fileinfo")
  466. ?.innerText.split(", ")[2];
  467. thumbnailUrl = thumbnailElement?.src;
  468. mediaLink = thumbnailElement?.parentNode.href;
  469. comment = post.querySelector("blockquote");
  470.  
  471. threadID = post.querySelector(".js").href.match(/thread\/(\d+)/)[1];
  472. postID = post.id.replace("pc", "").replace("p", "");
  473. break;
  474. case "archived.moe":
  475. case "archive.palanq.win":
  476. thumbnailUrl = post.querySelector(".post_image").src;
  477. mediaLink = post.querySelector(".thread_image_link").href;
  478. fileName = post.querySelector(
  479. ".post_file_filename"
  480. ).title;
  481. comment = post.querySelector(".text");
  482. threadID = post.querySelector(".post_data > a").href.match(
  483. /thread\/(\d+)/
  484. )[1];
  485. postID = post.id
  486. break;
  487. case "boards.4chan.org":
  488. case "boards.4channel.org":
  489. default:
  490. if (!post.querySelector(".fileText")) {
  491. return; // Skip posts without media links
  492. }
  493. // if they have 4chanX installed, there will be a fileText-orignal class
  494. if (post.querySelector(".download-button")) {
  495. temp = post.querySelector(".download-button");
  496. mediaLink = temp.href;
  497. fileName = temp.download;
  498. } else {
  499. if (post.classList.contains("opContainer")) {
  500. mediaLink = post.querySelector(".fileText a");
  501. temp = mediaLink;
  502. } else {
  503. mediaLink = post.querySelector(".fileText");
  504. temp = mediaLink.querySelector("a");
  505. }
  506. if (mediaLink.title === "") {
  507. if (temp.title === "") {
  508. fileName = temp.innerText;
  509. } else {
  510. fileName = temp.title;
  511. }
  512. } else {
  513. fileName = mediaLink.title;
  514. }
  515. mediaLink = temp.href;
  516. }
  517.  
  518. thumbnailUrl = post.querySelector(".fileThumb img")?.src;
  519. comment = post.querySelector(".postMessage");
  520. threadID = thread.match(/thread\/(\d+)/)[1];
  521. postID = post.id.replace("pc", "").replace("p", "");
  522. }
  523.  
  524. if (mediaLink) {
  525. isVideo = mediaLink.includes(".webm");
  526. isImage =
  527. mediaLink.includes(".jpg") ||
  528. mediaLink.includes(".png") ||
  529. mediaLink.includes(".gif");
  530. soundLink = fileName.match(/\[sound=(.+?)\]/);
  531. mediaLinkFlag = true;
  532. } else {
  533. return; // Skip posts without media links
  534. }
  535.  
  536. // replace the "#pcXXXXXXX" or "#pXXXXXXX" with an empty string to get the actual thread url
  537. if (thread.includes("#")) {
  538. postURL = thread.replace(/#p\d+/, "");
  539. postURL = postURL.replace(/#pc\d+/, "");
  540. } else {
  541. postURL = thread;
  542. }
  543.  
  544. // post info (constant)
  545. board = thread.match(/\/\/[^\/]+\/([^\/]+)/)[1];
  546. if (soundLink) {
  547. encodedSoundPostLink = `https://4chan.mahdeensky.top/${board}/thread/${threadID}/${postID}`;
  548. }
  549.  
  550. if (mediaLinkFlag) {
  551. // Check if the post should be loaded based on the mode
  552. if (
  553. mode === "all" ||
  554. (mode === "webm" && (isVideo || (isImage && soundLink)))
  555. ) {
  556. const cell = document.createElement("div");
  557. setStyles(cell, {
  558. border: "1px solid #d9d9d9",
  559. position: "relative",
  560. });
  561.  
  562. const buttonDiv = document.createElement("div");
  563. setStyles(buttonDiv, {
  564. display: "flex",
  565. justifyContent: "space-between",
  566. alignItems: "center",
  567. padding: "5px",
  568. });
  569.  
  570. if (isVideo) {
  571. const videoContainer = document.createElement("div");
  572. setStyles(videoContainer, {
  573. position: "relative",
  574. display: "flex",
  575. justifyContent: "center",
  576. });
  577.  
  578. const videoThumbnail = document.createElement("img");
  579. videoThumbnail.src = thumbnailUrl;
  580. videoThumbnail.alt = "Video Thumbnail";
  581. setStyles(videoThumbnail, {
  582. width: "100%",
  583. maxHeight: "200px",
  584. objectFit: "contain",
  585. cursor: "pointer",
  586. });
  587. videoThumbnail.loading = "lazy";
  588.  
  589. const video = document.createElement("video");
  590. video.src = mediaLink;
  591. video.muted = true;
  592. video.controls = true;
  593. video.title = comment.innerText;
  594. video.videothumbnailDisplayed = "true";
  595. video.setAttribute("fileName", fileName);
  596. video.setAttribute("board", board);
  597. video.setAttribute("threadID", threadID);
  598. video.setAttribute("postID", postID);
  599. setStyles(video, {
  600. maxWidth: "100%",
  601. maxHeight: "200px",
  602. objectFit: "contain",
  603. cursor: "pointer",
  604. display: "none",
  605. });
  606.  
  607. // videoJS stuff (not working for some reason)
  608. // video.className = "video-js";
  609. // video.setAttribute("data-setup", "{}");
  610. // const source = document.createElement("source");
  611. // source.src = mediaLink;
  612. // source.type = "video/webm";
  613. // video.appendChild(source);
  614.  
  615. videoThumbnail.addEventListener("click", () => {
  616. videoThumbnail.style.display = "none";
  617. video.style.display = "block";
  618. video.videothumbnailDisplayed = "false";
  619. video.load();
  620. });
  621.  
  622. // hide the video thumbnail and show the video when hovered
  623. videoThumbnail.addEventListener("mouseenter", () => {
  624. videoThumbnail.style.display = "none";
  625. video.style.display = "block";
  626. video.videothumbnailDisplayed = "false";
  627. video.load();
  628. });
  629.  
  630. // Play webms without sound automatically on hover or if autoPlayWebms is true
  631. if (!soundLink) {
  632. if (autoPlayWebms) {
  633. video.addEventListener("canplaythrough", () => {
  634. video.play();
  635. video.loop = true; // Loop webms when autoPlayWebms is true
  636. });
  637. } else {
  638. video.addEventListener("mouseenter", () => {
  639. video.play();
  640. });
  641. video.addEventListener("mouseleave", () => {
  642. video.pause();
  643. });
  644. }
  645. }
  646.  
  647. videoContainer.appendChild(videoThumbnail);
  648. videoContainer.appendChild(video);
  649.  
  650. if (soundLink) {
  651. // video.preload = "none"; // Disable video preload for better performance
  652.  
  653. const audio = document.createElement("audio");
  654. audio.src = decodeURIComponent(
  655. soundLink[1].startsWith("http")
  656. ? soundLink[1]
  657. : `https://${soundLink[1]}`
  658. );
  659. // add attribute to the audio element with the encoded soundpost link
  660. audio.setAttribute(
  661. "encodedSoundPostLink",
  662. encodedSoundPostLink
  663. );
  664. videoContainer.appendChild(audio);
  665.  
  666. const resetButton = document.createElement("button");
  667. resetButton.textContent = "Reset";
  668. setStyles(resetButton, {
  669. backgroundColor: "#1c1c1c",
  670. color: "#d9d9d9",
  671. padding: "5px 10px",
  672. borderRadius: "3px",
  673. border: "none",
  674. cursor: "pointer",
  675. boxShadow: "0 2px 4px rgba(0, 0, 0, 0.3)",
  676. });
  677. resetButton.addEventListener("click", () => {
  678. video.currentTime = 0;
  679. audio.currentTime = 0;
  680. });
  681. buttonDiv.appendChild(resetButton);
  682.  
  683. // html5 video play
  684. video.onplay = (event) => {
  685. audio.play();
  686. };
  687.  
  688. video.onpause = (event) => {
  689. audio.pause();
  690. };
  691.  
  692. let lastVideoTime = 0;
  693. // Sync audio with video on timeupdate event only if the difference is 2 seconds or more
  694. video.addEventListener("timeupdate", () => {
  695. if (Math.abs(video.currentTime - lastVideoTime) >= 2) {
  696. audio.currentTime = video.currentTime;
  697. lastVideoTime = video.currentTime;
  698. }
  699. lastVideoTime = video.currentTime;
  700. });
  701. }
  702.  
  703. cell.appendChild(videoContainer);
  704. } else if (isImage) {
  705. const imageContainer = document.createElement("div");
  706. setStyles(imageContainer, {
  707. position: "relative",
  708. display: "flex",
  709. justifyContent: "center",
  710. alignItems: "center",
  711. });
  712.  
  713. const image = document.createElement("img");
  714. image.src = thumbnailUrl;
  715. if (settings.Load_High_Res_Images_By_Default.value) {
  716. image.src = mediaLink;
  717. }
  718. if (mediaLink.includes(".gif")) {
  719. image.src = mediaLink;
  720. }
  721. image.setAttribute("fileName", fileName);
  722. image.setAttribute("actualSrc", mediaLink);
  723. image.setAttribute("thumbnailUrl", thumbnailUrl);
  724. image.setAttribute("board", board);
  725. image.setAttribute("threadID", threadID);
  726. image.setAttribute("postID", postID);
  727. setStyles(image, {
  728. maxWidth: "100%",
  729. maxHeight: "200px",
  730. objectFit: "contain",
  731. cursor: "pointer",
  732. });
  733.  
  734. let createDarkenBackground = () => {
  735. const background = document.createElement("div");
  736. background.id = "darkenBackground";
  737. setStyles(background, {
  738. position: "fixed",
  739. top: "0",
  740. left: "0",
  741. width: "100%",
  742. height: "100%",
  743. backgroundColor: "rgba(0, 0, 0, 0.3)",
  744. backdropFilter: "blur(5px)",
  745. zIndex: "9999",
  746. });
  747. return background;
  748. };
  749.  
  750. let zoomImage = () => {
  751. // have the image pop up centered in front of the screen so that it fills about 80% of the screen
  752. image.style = "";
  753. image.src = mediaLink;
  754. setStyles(image, {
  755. position: "fixed",
  756. top: "50%",
  757. left: "50%",
  758. transform: "translate(-50%, -50%)",
  759. zIndex: "10000",
  760. height: "80%",
  761. width: "80%",
  762. objectFit: "contain",
  763. cursor: "pointer",
  764. });
  765.  
  766. // darken and blur the background behind the image without affecting the image
  767. const background = createDarkenBackground();
  768. gallery.appendChild(background);
  769.  
  770. // create a container for the buttons, number, and download buttons (even space between them)
  771. // position: fixed; bottom: 10px; display: flex; flex-direction: row; justify-content: space-around; z-index: 10000; width: 100%; margin:auto;
  772. const bottomContainer = document.createElement("div");
  773. setStyles(bottomContainer, {
  774. position: "fixed",
  775. bottom: "10px",
  776. display: "flex",
  777. flexDirection: "row",
  778. justifyContent: "space-around",
  779. zIndex: "10000",
  780. width: "100%",
  781. margin: "auto",
  782. });
  783. background.appendChild(bottomContainer);
  784.  
  785. // buttons on the bottom left of the screen for reverse image search (SauceNAO, Google Lens, Yandex)
  786. const buttonContainer = document.createElement("div");
  787. setStyles(buttonContainer, {
  788. display: "flex",
  789. gap: "10px",
  790. });
  791. buttonContainer.setAttribute("mediaLink", mediaLink);
  792.  
  793. const sauceNAOButton = document.createElement("button");
  794. sauceNAOButton.textContent = "SauceNAO";
  795. setStyles(sauceNAOButton, {
  796. backgroundColor: "#1c1c1c",
  797. color: "#d9d9d9",
  798. padding: "5px 10px",
  799. borderRadius: "3px",
  800. border: "none",
  801. cursor: "pointer",
  802. });
  803. sauceNAOButton.addEventListener("click", () => {
  804. window.open(
  805. `https://saucenao.com/search.php?url=${encodeURIComponent(
  806. buttonContainer.getAttribute("mediaLink")
  807. )}`
  808. );
  809. });
  810. buttonContainer.appendChild(sauceNAOButton);
  811.  
  812. const googleLensButton = document.createElement("button");
  813. googleLensButton.textContent = "Google Lens";
  814. setStyles(googleLensButton, {
  815. backgroundColor: "#1c1c1c",
  816. color: "#d9d9d9",
  817. padding: "5px 10px",
  818. borderRadius: "3px",
  819. border: "none",
  820. cursor: "pointer",
  821. boxShadow: "0 2px 4px rgba(0, 0, 0, 0.3)",
  822. });
  823. googleLensButton.addEventListener("click", () => {
  824. window.open(
  825. `https://lens.google.com/uploadbyurl?url=${encodeURIComponent(
  826. buttonContainer.getAttribute("mediaLink")
  827. )}`
  828. );
  829. });
  830. buttonContainer.appendChild(googleLensButton);
  831.  
  832. const yandexButton = document.createElement("button");
  833. yandexButton.textContent = "Yandex";
  834. setStyles(yandexButton, {
  835. backgroundColor: "#1c1c1c",
  836. color: "#d9d9d9",
  837. padding: "5px 10px",
  838. borderRadius: "3px",
  839. border: "none",
  840. cursor: "pointer",
  841. boxShadow: "0 2px 4px rgba(0, 0, 0, 0.3)",
  842. });
  843. yandexButton.addEventListener("click", () => {
  844. window.open(
  845. `https://yandex.com/images/search?rpt=imageview&url=${encodeURIComponent(
  846. buttonContainer.getAttribute("mediaLink")
  847. )}`
  848. );
  849. });
  850. buttonContainer.appendChild(yandexButton);
  851.  
  852. bottomContainer.appendChild(buttonContainer);
  853.  
  854. // download container for video/img and audio
  855. const downloadButtonContainer =
  856. document.createElement("div");
  857. setStyles(downloadButtonContainer, {
  858. display: "flex",
  859. gap: "10px",
  860. });
  861. bottomContainer.appendChild(downloadButtonContainer);
  862.  
  863. const viewPostButton = document.createElement("a");
  864. viewPostButton.textContent = "View Post";
  865. viewPostButton.href = `https://boards.4chan.org/${board}/thread/${threadID}#p${postID}`;
  866. setStyles(viewPostButton, {
  867. backgroundColor: "#1c1c1c",
  868. color: "#d9d9d9",
  869. padding: "5px 10px",
  870. borderRadius: "3px",
  871. border: "none",
  872. cursor: "pointer",
  873. boxShadow: "0 2px 4px rgba(0, 0, 0, 0.3)",
  874. });
  875. downloadButtonContainer.appendChild(viewPostButton);
  876.  
  877. const downloadButton = document.createElement("a");
  878. downloadButton.textContent = "Download Video/Image";
  879. downloadButton.href = mediaLink;
  880. downloadButton.download = fileName;
  881. downloadButton.target = "_blank";
  882. setStyles(downloadButton, {
  883. backgroundColor: "#1c1c1c",
  884. color: "#d9d9d9",
  885. padding: "5px 10px",
  886. borderRadius: "3px",
  887. border: "none",
  888. cursor: "pointer",
  889. boxShadow: "0 2px 4px rgba(0, 0, 0, 0.3)",
  890. });
  891. downloadButtonContainer.appendChild(downloadButton);
  892.  
  893. const audioDownloadButton = document.createElement("a");
  894. audioDownloadButton.textContent = "Download Audio";
  895. audioDownloadButton.target = "_blank";
  896. setStyles(audioDownloadButton, {
  897. backgroundColor: "#1c1c1c",
  898. color: "#d9d9d9",
  899. padding: "5px 10px",
  900. borderRadius: "3px",
  901. border: "none",
  902. cursor: "pointer",
  903. boxShadow: "0 2px 4px rgba(0, 0, 0, 0.3)",
  904. });
  905. if (soundLink) {
  906. audioDownloadButton.href = decodeURIComponent(
  907. soundLink[1].startsWith("http")
  908. ? soundLink[1]
  909. : `https://${soundLink[1]}`
  910. );
  911. audioDownloadButton.download = soundLink[1]
  912. .split("/")
  913. .pop();
  914. } else {
  915. audioDownloadButton.style.display = "none";
  916. }
  917. downloadButtonContainer.appendChild(audioDownloadButton);
  918.  
  919. // a button beside the download video and download audio button that says download encoded soundpost which links to the following url in a new tab "https://4chan.mahdeensky.top/<board>/thread/<thread>/<post>" where things between the <>, are variables to be replaced
  920. const encodedSoundPostButton =
  921. document.createElement("a");
  922. encodedSoundPostButton.textContent =
  923. "Download Encoded Soundpost";
  924. encodedSoundPostButton.target = "_blank";
  925. setStyles(encodedSoundPostButton, {
  926. backgroundColor: "#1c1c1c",
  927. color: "#d9d9d9",
  928. padding: "5px 10px",
  929. borderRadius: "3px",
  930. border: "none",
  931. cursor: "pointer",
  932. boxShadow: "0 2px 4px rgba(0, 0, 0, 0.3)",
  933. });
  934. if (soundLink) {
  935. encodedSoundPostButton.href = `https://4chan.mahdeensky.top/${board}/thread/${threadID}/${postID}`;
  936. } else {
  937. encodedSoundPostButton.style.display = "none";
  938. }
  939. downloadButtonContainer.appendChild(
  940. encodedSoundPostButton
  941. );
  942.  
  943. // number on the bottom right of the screen to show which image is currently being viewed
  944. const imageNumber = document.createElement("div");
  945. let currentImageNumber =
  946. Array.from(cell.parentNode.children).indexOf(cell) + 1;
  947. let imageTotal = cell.parentNode.children.length;
  948. imageNumber.textContent = `${currentImageNumber}/${imageTotal}`;
  949. setStyles(imageNumber, {
  950. backgroundColor: "#1c1c1c",
  951. color: "#d9d9d9",
  952. padding: "5px 10px",
  953. borderRadius: "3px",
  954. border: "none",
  955. cursor: "pointer",
  956. boxShadow: "0 2px 4px rgba(0, 0, 0, 0.3)",
  957. zIndex: "10000",
  958. });
  959. bottomContainer.appendChild(imageNumber);
  960.  
  961. // title of the image/video on the top left of the screen
  962. const imageTitle = document.createElement("div");
  963. imageTitle.textContent = fileName;
  964. setStyles(imageTitle, {
  965. position: "fixed",
  966. top: "10px",
  967. left: "10px",
  968. backgroundColor: "#1c1c1c",
  969. color: "#d9d9d9",
  970. padding: "5px 10px",
  971. borderRadius: "3px",
  972. border: "none",
  973. cursor: "pointer",
  974. boxShadow: "0 2px 4px rgba(0, 0, 0, 0.3)",
  975. zIndex: "10000",
  976. });
  977. background.appendChild(imageTitle);
  978.  
  979. let currentCell = cell;
  980. // use left and right arrow keys to navigate between images/videos
  981. let keybindHandler = (event) => {
  982. if (event.key === "ArrowLeft") {
  983. // get the previous cell in the grid
  984. const previousCell =
  985. currentCell.previousElementSibling;
  986. if (previousCell) {
  987. if (gallery.querySelector("#zoomedVideo")) {
  988. if (
  989. gallery
  990. .querySelector("#zoomedVideo")
  991. .querySelector("audio")
  992. ) {
  993. gallery
  994. .querySelector("#zoomedVideo")
  995. .querySelector("audio")
  996. .pause();
  997. }
  998. gallery.removeChild(
  999. gallery.querySelector("#zoomedVideo")
  1000. );
  1001. } else if (gallery.querySelector("#zoomedImage")) {
  1002. gallery.removeChild(
  1003. gallery.querySelector("#zoomedImage")
  1004. );
  1005. } else {
  1006. image.style = "";
  1007. // image.src = thumbnailUrl;
  1008. setStyles(image, {
  1009. maxWidth: "100%",
  1010. maxHeight: "200px",
  1011. objectFit: "contain",
  1012. });
  1013. }
  1014.  
  1015. // check if it has a video
  1016. const video = previousCell?.querySelector("video");
  1017. if (video) {
  1018. const video = previousCell
  1019. .querySelector("video")
  1020. .cloneNode(true);
  1021. video.id = "zoomedVideo";
  1022. video.style = "";
  1023. setStyles(video, {
  1024. position: "fixed",
  1025. top: "50%",
  1026. left: "50%",
  1027. transform: "translate(-50%, -50%)",
  1028. zIndex: "10000",
  1029. height: "80%",
  1030. width: "80%",
  1031. objectFit: "contain",
  1032. cursor: "pointer",
  1033. preload: "auto",
  1034. });
  1035. gallery.appendChild(video);
  1036.  
  1037. // check if there is an audio element
  1038. let audio = previousCell.querySelector("audio");
  1039. if (audio) {
  1040. audio = audio.cloneNode(true);
  1041.  
  1042. // same event listeners as the video
  1043. video.onplay = (event) => {
  1044. audio.play();
  1045. };
  1046.  
  1047. video.onpause = (event) => {
  1048. audio.pause();
  1049. };
  1050.  
  1051. let lastVideoTime = 0;
  1052. video.addEventListener("timeupdate", () => {
  1053. if (
  1054. Math.abs(
  1055. video.currentTime - lastVideoTime
  1056. ) >= 2
  1057. ) {
  1058. audio.currentTime = video.currentTime;
  1059. lastVideoTime = video.currentTime;
  1060. }
  1061. lastVideoTime = video.currentTime;
  1062. });
  1063. video.appendChild(audio);
  1064. }
  1065. } else {
  1066. // if it doesn't have a video, it must have an image
  1067. const originalImage =
  1068. previousCell.querySelector("img");
  1069. const currentImage =
  1070. originalImage.cloneNode(true);
  1071. currentImage.id = "zoomedImage";
  1072. currentImage.style = "";
  1073. currentImage.src =
  1074. currentImage.getAttribute("actualSrc");
  1075. originalImage.src =
  1076. originalImage.getAttribute("actualSrc");
  1077. setStyles(currentImage, {
  1078. position: "fixed",
  1079. top: "50%",
  1080. left: "50%",
  1081. transform: "translate(-50%, -50%)",
  1082. zIndex: "10000",
  1083. height: "80%",
  1084. width: "80%",
  1085. objectFit: "contain",
  1086. cursor: "pointer",
  1087. });
  1088. gallery.appendChild(currentImage);
  1089. currentImage.addEventListener("click", () => {
  1090. gallery.removeChild(currentImage);
  1091. gallery.removeChild(background);
  1092. document.removeEventListener(
  1093. "keydown",
  1094. keybindHandler
  1095. );
  1096. });
  1097.  
  1098. let audio = previousCell.querySelector("audio");
  1099. if (audio) {
  1100. audio = audio.cloneNode(true);
  1101. currentImage.appendChild(audio);
  1102.  
  1103. // event listeners when hovering over the image
  1104. currentImage.addEventListener(
  1105. "mouseenter",
  1106. () => {
  1107. audio.play();
  1108. }
  1109. );
  1110. currentImage.addEventListener(
  1111. "mouseleave",
  1112. () => {
  1113. audio.pause();
  1114. }
  1115. );
  1116. }
  1117. }
  1118.  
  1119. if (previousCell) {
  1120. currentCell = previousCell;
  1121. buttonContainer.setAttribute(
  1122. "mediaLink",
  1123. previousCell.querySelector("img").src
  1124. );
  1125.  
  1126. currentImageNumber -= 1;
  1127. imageNumber.textContent = `${currentImageNumber}/${imageTotal}`;
  1128.  
  1129. // filename of the video if it has one, otherwise the filename of the image
  1130. imageTitle.textContent = video
  1131. ? video.getAttribute("fileName")
  1132. : previousCell
  1133. .querySelector("img")
  1134. .getAttribute("fileName");
  1135.  
  1136. // update view post button link
  1137. let previousMedia = video || previousCell.querySelector("img");
  1138. let previousBoard = previousMedia.getAttribute("board");
  1139. let previousThreadID = previousMedia.getAttribute("threadID");
  1140. let previousPostID = previousMedia.getAttribute("postID");
  1141. viewPostButton.href = `https://boards.4chan.org/${previousBoard}/thread/${previousThreadID}#p${previousPostID}`;
  1142.  
  1143. // update the download button links
  1144. downloadButton.href = previousMedia.src;
  1145. if (previousCell.querySelector("audio")) {
  1146. // updating audio button download link
  1147. audioDownloadButton.href =
  1148. previousCell.querySelector("audio").src;
  1149. audioDownloadButton.download = previousCell
  1150. .querySelector("audio")
  1151. .src.split("/")
  1152. .pop();
  1153. audioDownloadButton.style.display = "block";
  1154.  
  1155. // updating encoded soundpost button link
  1156. encodedSoundPostButton.href = previousCell.querySelector("audio")
  1157. .getAttribute("encodedSoundPostLink");
  1158. encodedSoundPostButton.style.display = "block";
  1159.  
  1160. } else {
  1161. audioDownloadButton.style.display = "none";
  1162. encodedSoundPostButton.style.display = "none";
  1163. }
  1164. }
  1165. }
  1166. } else if (event.key === "ArrowRight") {
  1167. // get the next cell in the grid
  1168. const nextCell = currentCell.nextElementSibling;
  1169. if (nextCell) {
  1170. if (gallery.querySelector("#zoomedVideo")) {
  1171. if (
  1172. gallery
  1173. .querySelector("#zoomedVideo")
  1174. .querySelector("audio")
  1175. ) {
  1176. gallery
  1177. .querySelector("#zoomedVideo")
  1178. .querySelector("audio")
  1179. .pause();
  1180. }
  1181. gallery.removeChild(
  1182. gallery.querySelector("#zoomedVideo")
  1183. );
  1184. // ("removed video");
  1185. } else if (gallery.querySelector("#zoomedImage")) {
  1186. gallery.removeChild(
  1187. gallery.querySelector("#zoomedImage")
  1188. );
  1189. // ("removed image");
  1190. } else {
  1191. image.style = "";
  1192. setStyles(image, {
  1193. maxWidth: "100%",
  1194. maxHeight: "200px",
  1195. objectFit: "contain",
  1196. });
  1197. }
  1198.  
  1199. // check if it has a video
  1200. const video = nextCell?.querySelector("video");
  1201. if (video) {
  1202. const video = nextCell
  1203. .querySelector("video")
  1204. .cloneNode(true);
  1205. video.id = "zoomedVideo";
  1206. video.style = "";
  1207. setStyles(video, {
  1208. position: "fixed",
  1209. top: "50%",
  1210. left: "50%",
  1211. transform: "translate(-50%, -50%)",
  1212. zIndex: "10000",
  1213. height: "80%",
  1214. width: "80%",
  1215. objectFit: "contain",
  1216. cursor: "pointer",
  1217. preload: "auto",
  1218. });
  1219.  
  1220. // check if there is an audio element
  1221. let audio = nextCell.querySelector("audio");
  1222. if (audio) {
  1223. audio = audio.cloneNode(true);
  1224.  
  1225. // same event listeners as the video
  1226. video.onplay = (event) => {
  1227. audio.play();
  1228. };
  1229.  
  1230. video.onpause = (event) => {
  1231. audio.pause();
  1232. };
  1233.  
  1234. let lastVideoTime = 0;
  1235. video.addEventListener("timeupdate", () => {
  1236. if (
  1237. Math.abs(
  1238. video.currentTime - lastVideoTime
  1239. ) >= 2
  1240. ) {
  1241. audio.currentTime = video.currentTime;
  1242. lastVideoTime = video.currentTime;
  1243. }
  1244. lastVideoTime = video.currentTime;
  1245. });
  1246. video.appendChild(audio);
  1247. }
  1248. gallery.appendChild(video);
  1249. } else {
  1250. const originalImage =
  1251. nextCell.querySelector("img");
  1252. const currentImage =
  1253. originalImage.cloneNode(true);
  1254. currentImage.id = "zoomedImage";
  1255. currentImage.style = "";
  1256. currentImage.src =
  1257. currentImage.getAttribute("actualSrc");
  1258. originalImage.src =
  1259. originalImage.getAttribute("actualSrc");
  1260. setStyles(currentImage, {
  1261. position: "fixed",
  1262. top: "50%",
  1263. left: "50%",
  1264. transform: "translate(-50%, -50%)",
  1265. zIndex: "10000",
  1266. height: "80%",
  1267. width: "80%",
  1268. objectFit: "contain",
  1269. cursor: "pointer",
  1270. });
  1271. gallery.appendChild(currentImage);
  1272. currentImage.addEventListener("click", () => {
  1273. gallery.removeChild(currentImage);
  1274. gallery.removeChild(background);
  1275. document.removeEventListener(
  1276. "keydown",
  1277. keybindHandler
  1278. );
  1279. });
  1280.  
  1281. let audio = nextCell.querySelector("audio");
  1282. if (audio) {
  1283. audio = nextCell
  1284. .querySelector("audio")
  1285. .cloneNode(true);
  1286. currentImage.appendChild(audio);
  1287.  
  1288. currentImage.addEventListener(
  1289. "mouseenter",
  1290. () => {
  1291. audio.play();
  1292. }
  1293. );
  1294. currentImage.addEventListener(
  1295. "mouseleave",
  1296. () => {
  1297. audio.pause();
  1298. }
  1299. );
  1300. }
  1301. }
  1302. if (nextCell) {
  1303. currentCell = nextCell;
  1304. buttonContainer.setAttribute(
  1305. "mediaLink",
  1306. nextCell.querySelector("img").src
  1307. );
  1308.  
  1309. currentImageNumber += 1;
  1310. imageNumber.textContent = `${currentImageNumber}/${imageTotal}`;
  1311.  
  1312. // filename of the video if it has one, otherwise the filename of the image
  1313. imageTitle.textContent = video
  1314. ? video.getAttribute("fileName")
  1315. : nextCell
  1316. .querySelector("img")
  1317. .getAttribute("fileName");
  1318.  
  1319. // update view post button link
  1320. let nextMedia = video || nextCell.querySelector("img");
  1321. let nextBoard = nextMedia.getAttribute("board");
  1322. let nextThreadID = nextMedia.getAttribute("threadID");
  1323. let nextPostID = nextMedia.getAttribute("postID");
  1324. viewPostButton.href = `https://boards.4chan.org/${nextBoard}/thread/${nextThreadID}#p${nextPostID}`;
  1325.  
  1326. // update the download button links
  1327. downloadButton.href = nextMedia.src;
  1328. if (nextCell.querySelector("audio")) {
  1329. audioDownloadButton.href =
  1330. nextCell.querySelector("audio").src;
  1331. audioDownloadButton.download = nextCell
  1332. .querySelector("audio")
  1333. .src.split("/")
  1334. .pop();
  1335. audioDownloadButton.style.display = "block";
  1336.  
  1337. encodedSoundPostButton.href = nextCell.querySelector("audio")
  1338. .getAttribute("encodedSoundPostLink");
  1339. encodedSoundPostButton.style.display = "block";
  1340. } else {
  1341. audioDownloadButton.style.display = "none";
  1342. encodedSoundPostButton.style.display = "none";
  1343. }
  1344. }
  1345. }
  1346. }
  1347. };
  1348. document.addEventListener("keydown", keybindHandler);
  1349.  
  1350. image.addEventListener(
  1351. "click",
  1352. () => {
  1353. image.style = "";
  1354. // image.src = thumbnailUrl;
  1355. setStyles(image, {
  1356. maxWidth: "99%",
  1357. maxHeight: "199px",
  1358. objectFit: "contain",
  1359. });
  1360.  
  1361. if (gallery.querySelector("#darkenBackground")) {
  1362. gallery.removeChild(background);
  1363. }
  1364. document.removeEventListener(
  1365. "keydown",
  1366. keybindHandler
  1367. );
  1368.  
  1369. image.addEventListener("click", zoomImage, {
  1370. once: true,
  1371. });
  1372. },
  1373. { once: true }
  1374. );
  1375. };
  1376.  
  1377. image.addEventListener("click", zoomImage, { once: true });
  1378. image.title = comment.innerText;
  1379. image.loading = "lazy";
  1380.  
  1381. if (soundLink) {
  1382. const audio = document.createElement("audio");
  1383. audio.src = decodeURIComponent(
  1384. soundLink[1].startsWith("http")
  1385. ? soundLink[1]
  1386. : `https://${soundLink[1]}`
  1387. );
  1388. audio.loop = true;
  1389. // set the attribute to the audio element with the encoded soundpost link
  1390. audio.setAttribute(
  1391. "encodedSoundPostLink",
  1392. encodedSoundPostLink
  1393. );
  1394. imageContainer.appendChild(audio);
  1395.  
  1396. image.addEventListener("mouseenter", () => {
  1397. audio.play();
  1398. });
  1399. image.addEventListener("mouseleave", () => {
  1400. audio.pause();
  1401. });
  1402.  
  1403. const playPauseButton = document.createElement("button");
  1404. playPauseButton.textContent = "Play/Pause";
  1405. setStyles(playPauseButton, {
  1406. backgroundColor: "#1c1c1c",
  1407. color: "#d9d9d9",
  1408. padding: "5px 10px",
  1409. borderRadius: "3px",
  1410. border: "none",
  1411. cursor: "pointer",
  1412. boxShadow: "0 2px 4px rgba(0, 0, 0, 0.3)",
  1413. });
  1414. playPauseButton.addEventListener("click", () => {
  1415. if (audio.paused) {
  1416. audio.play();
  1417. } else {
  1418. audio.pause();
  1419. }
  1420. });
  1421. buttonDiv.appendChild(playPauseButton);
  1422. }
  1423. imageContainer.appendChild(image);
  1424. cell.appendChild(imageContainer);
  1425. } else {
  1426. return; // Skip non-video and non-image posts
  1427. }
  1428.  
  1429. // Add button that scrolls to the post in the thread
  1430. const viewPostButton = document.createElement("button");
  1431. viewPostButton.textContent = "View Post";
  1432. setStyles(viewPostButton, {
  1433. backgroundColor: "#1c1c1c",
  1434. color: "#d9d9d9",
  1435. padding: "5px 10px",
  1436. borderRadius: "3px",
  1437. border: "none",
  1438. cursor: "pointer",
  1439. boxShadow: "0 2px 4px rgba(0, 0, 0, 0.3)",
  1440. });
  1441.  
  1442. viewPostButton.addEventListener("click", () => {
  1443. // post id example: "pc77515440"
  1444. window.location.href = postURL + "#" + post.id;
  1445. document.body.removeChild(gallery);
  1446. });
  1447. buttonDiv.appendChild(viewPostButton);
  1448.  
  1449. cell.appendChild(buttonDiv);
  1450. gridContainer.appendChild(cell);
  1451. }
  1452. }
  1453. });
  1454. });
  1455. };
  1456. checkedThreads.forEach(loadPostsFromThread);
  1457. };
  1458.  
  1459. loadPosts(mode);
  1460.  
  1461. gallery.appendChild(gridContainer);
  1462.  
  1463. const closeButton = document.createElement("button");
  1464. closeButton.textContent = "Close";
  1465. closeButton.id = "closeGallery";
  1466. setStyles(closeButton, {
  1467. position: "absolute",
  1468. bottom: "10px",
  1469. right: "10px",
  1470. zIndex: "10000",
  1471. backgroundColor: "#1c1c1c",
  1472. color: "#d9d9d9",
  1473. padding: "10px 20px",
  1474. borderRadius: "5px",
  1475. border: "none",
  1476. cursor: "pointer",
  1477. boxShadow: "0 2px 4px rgba(0, 0, 0, 0.3)",
  1478. });
  1479. closeButton.addEventListener("click", () => {
  1480. gallerySize = {
  1481. width: gridContainer.offsetWidth,
  1482. height: gridContainer.offsetHeight,
  1483. };
  1484. document.body.removeChild(gallery);
  1485. });
  1486. gallery.appendChild(closeButton);
  1487.  
  1488. document.body.appendChild(gallery);
  1489.  
  1490. // Store the current scroll position and grid container size when closing the gallery
  1491. // (`Last scroll position: ${lastScrollPosition} px`);
  1492. gridContainer.addEventListener("scroll", () => {
  1493. lastScrollPosition = gridContainer.scrollTop;
  1494. // (`Current scroll position: ${lastScrollPosition} px`);
  1495. });
  1496.  
  1497. // Restore the last scroll position and grid container size when opening the gallery after a timeout if the url is the same
  1498. if (window.location.href === threadURL) {
  1499. setTimeout(() => {
  1500. if (gallerySize.width > 0 && gallerySize.height > 0) {
  1501. gridContainer.style.width = `${gallerySize.width}px`;
  1502. gridContainer.style.height = `${gallerySize.height}px`;
  1503. }
  1504. // (`Restored scroll position: ${lastScrollPosition} px`);
  1505. gridContainer.scrollTop = lastScrollPosition;
  1506. }, 200);
  1507. } else {
  1508. // Reset the last scroll position and grid container size if the url is different
  1509. threadURL = window.location.href;
  1510. lastScrollPosition = 0;
  1511. gallerySize = { width: 0, height: 0 };
  1512. }
  1513. };
  1514.  
  1515. button.addEventListener("click", openImageGallery);
  1516.  
  1517. // Append the button to the body
  1518. document.body.appendChild(button);
  1519.  
  1520. if (isArchivePage) {
  1521. // adds the category to thead
  1522. const thead = document.querySelector(".flashListing thead tr");
  1523. const checkboxCell = document.createElement("td");
  1524. checkboxCell.className = "postblock";
  1525. checkboxCell.textContent = "Selected";
  1526. thead.insertBefore(checkboxCell, thead.firstChild);
  1527.  
  1528. // Add checkboxes to each thread row
  1529. const threadRows = document.querySelectorAll(".flashListing tbody tr");
  1530. threadRows.forEach((row) => {
  1531. const checkbox = document.createElement("input");
  1532. checkbox.type = "checkbox";
  1533. const checkboxCell = document.createElement("td");
  1534. checkboxCell.appendChild(checkbox);
  1535. row.insertBefore(checkboxCell, row.firstChild);
  1536. });
  1537. }
  1538. };
  1539.  
  1540. // Use the "i" key to open and close the gallery/grid
  1541. document.addEventListener("keydown", (event) => {
  1542. if (event.key === "i") {
  1543. // Prevent the gallery from opening when typing in an input or textarea
  1544. if (
  1545. event.target.tagName == "INPUT" ||
  1546. event.target.tagName == "TEXTAREA"
  1547. ) {
  1548. return;
  1549. }
  1550.  
  1551. if (document.querySelector("#imageGallery")) {
  1552. document.body.removeChild(document.querySelector("#imageGallery"));
  1553. } else {
  1554. if (document.querySelector("#openImageGallery")) {
  1555. document.querySelector("#openImageGallery").click();
  1556. }
  1557. }
  1558. }
  1559. });
  1560.  
  1561. loadButton();
  1562. ("4chan Gallery loaded successfully!");
  1563. })();