4chan Gallery

4chan grid based Image Gallery for threads that can load images, images with sounds, webms with sounds (Button on the Bottom Right)

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

  1. // ==UserScript==
  2. // @name 4chan Gallery
  3. // @namespace http://tampermonkey.net/
  4. // @version 2024-03-26 (1.3)
  5. // @description 4chan grid based Image Gallery for threads that can load images, images with sounds, webms with sounds (Button on the Bottom Right)
  6. // @author TheDarkEnjoyer
  7. // @match http://boards.4chan.org/*/thread/*
  8. // @match https://boards.4chan.org/*/thread/*
  9. // @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
  10. // @grant none
  11. // @license GNU GPLv3
  12. // ==/UserScript==
  13.  
  14. (function () {
  15. "use strict";
  16.  
  17. let threadURL = window.location.href;
  18. let lastScrollPosition = 0;
  19. let gallerySize = { width: 0, height: 0 };
  20.  
  21. function setStyles(element, styles) {
  22. for (const property in styles) {
  23. element.style[property] = styles[property];
  24. }
  25. }
  26.  
  27. // Function to load the button
  28. const loadButton = () => {
  29. const posts = document.querySelectorAll(".postContainer");
  30.  
  31. // Check if there are at least two posts
  32. if (posts.length >= 2) {
  33. const button = document.createElement("button");
  34. button.textContent = "Open Image Gallery";
  35. setStyles(button, {
  36. position: "fixed",
  37. bottom: "20px",
  38. right: "20px",
  39. zIndex: "1000",
  40. backgroundColor: "var(--main-color)",
  41. color: "var(--text-color)",
  42. });
  43.  
  44. // Append the button to the body after 2 seconds
  45. setTimeout(() => document.body.appendChild(button), 2000);
  46.  
  47. const openImageGallery = () => {
  48. const gallery = document.createElement("div");
  49. setStyles(gallery, {
  50. position: "fixed",
  51. top: "0",
  52. left: "0",
  53. width: "100%",
  54. height: "100%",
  55. backgroundColor: "rgba(0, 0, 0, 0.8)",
  56. display: "flex",
  57. justifyContent: "center",
  58. alignItems: "center",
  59. zIndex: "9999",
  60. });
  61.  
  62. const gridContainer = document.createElement("div");
  63. setStyles(gridContainer, {
  64. display: "grid",
  65. gridTemplateColumns: "repeat(auto-fit, minmax(200px, 1fr))",
  66. gap: "10px",
  67. padding: "20px",
  68. backgroundColor: "var(--main-color)",
  69. color: "var(--text-color)",
  70. maxWidth: "80%",
  71. maxHeight: "80%",
  72. overflowY: "auto",
  73. resize: "both",
  74. overflow: "auto",
  75. border: "1px solid var(--text-color)",
  76. });
  77.  
  78. // Restore the previous grid container size
  79. if (gallerySize.width > 0 && gallerySize.height > 0) {
  80. gridContainer.style.width = `${gallerySize.width}px`;
  81. gridContainer.style.height = `${gallerySize.height}px`;
  82. }
  83.  
  84. let mode = "all"; // Default mode is "all"
  85. let autoPlayWebms = false; // Default auto play webms without sound is false
  86.  
  87. // Toggle mode button
  88. const toggleModeButton = document.createElement("button");
  89. toggleModeButton.textContent = "Toggle Mode (All)";
  90. setStyles(toggleModeButton, {
  91. position: "absolute",
  92. top: "10px",
  93. left: "10px",
  94. backgroundColor: "var(--main-color)",
  95. color: "var(--text-color)",
  96. });
  97. toggleModeButton.addEventListener("click", () => {
  98. mode = mode === "all" ? "webm" : "all";
  99. toggleModeButton.textContent = `Toggle Mode (${mode === "all" ? "All" : "Webm"})`;
  100. gridContainer.innerHTML = ""; // Clear the grid
  101. loadPosts(mode); // Reload posts based on the new mode
  102. });
  103. gallery.appendChild(toggleModeButton);
  104.  
  105. // Toggle auto play webms button
  106. const toggleAutoPlayButton = document.createElement("button");
  107. toggleAutoPlayButton.textContent = "Auto Play Webms without Sound";
  108. setStyles(toggleAutoPlayButton, {
  109. position: "absolute",
  110. top: "10px",
  111. left: "200px",
  112. backgroundColor: "var(--main-color)",
  113. color: "var(--text-color)",
  114. });
  115. toggleAutoPlayButton.addEventListener("click", () => {
  116. autoPlayWebms = !autoPlayWebms;
  117. toggleAutoPlayButton.textContent = autoPlayWebms ? "Stop Auto Play Webms" : "Auto Play Webms without Sound";
  118. gridContainer.innerHTML = ""; // Clear the grid
  119. loadPosts(mode); // Reload posts based on the new mode and auto play setting
  120. });
  121. gallery.appendChild(toggleAutoPlayButton);
  122.  
  123. const loadPosts = (mode) => {
  124. // Loop through each post and add its image/video to the grid
  125. posts.forEach((post) => {
  126. const mediaLink = post.querySelector(".fileText a");
  127. const comment = post.querySelector(".postMessage");
  128.  
  129. if (mediaLink) {
  130. const isVideo = mediaLink.href.includes(".webm");
  131. const fileName = mediaLink.href.split("/").pop();
  132. const soundLink = mediaLink.title.match(/\[sound=(.+?)\]/);
  133.  
  134. // Check if the post should be loaded based on the mode
  135. if (mode === "all" || (mode === "webm" && isVideo)) {
  136. const cell = document.createElement("div");
  137. setStyles(cell, {
  138. border: "1px solid var(--text-color)",
  139. position: "relative",
  140. });
  141.  
  142. const buttonDiv = document.createElement("div");
  143. setStyles(buttonDiv, {
  144. display: "flex",
  145. justifyContent: "space-between",
  146. alignItems: "center",
  147. padding: "5px",
  148. });
  149.  
  150. if (isVideo) {
  151. const videoContainer = document.createElement("div");
  152. setStyles(videoContainer, {
  153. position: "relative",
  154. });
  155.  
  156. const video = document.createElement("video");
  157. video.src = mediaLink.href;
  158. setStyles(video, {
  159. maxWidth: "100%",
  160. maxHeight: "200px",
  161. objectFit: "contain",
  162. cursor: "pointer",
  163. });
  164. video.muted = true;
  165. video.addEventListener("click", () => {
  166. post.scrollIntoView({ behavior: "smooth" });
  167. gallerySize = {
  168. width: gridContainer.offsetWidth,
  169. height: gridContainer.offsetHeight,
  170. };
  171. document.body.removeChild(gallery);
  172. });
  173. video.controls = true;
  174. video.title = comment.textContent;
  175.  
  176. // Play webms without sound automatically on hover or if autoPlayWebms is true
  177. if (!soundLink) {
  178. if (autoPlayWebms) {
  179. video.play();
  180. video.loop = true; // Loop webms when autoPlayWebms is true
  181. } else {
  182. video.addEventListener("mouseenter", () => {
  183. video.play();
  184. });
  185. video.addEventListener("mouseleave", () => {
  186. video.pause();
  187. });
  188. }
  189. }
  190.  
  191. videoContainer.appendChild(video);
  192.  
  193. if (soundLink) {
  194. const audio = document.createElement("audio");
  195. audio.src = decodeURIComponent(soundLink[1].startsWith("http") ? soundLink[1] : `https://${soundLink[1]}`);
  196. videoContainer.appendChild(audio);
  197.  
  198. const playPauseButton = document.createElement("button");
  199. playPauseButton.textContent = "Play/Pause";
  200. playPauseButton.addEventListener("click", () => {
  201. if (video.paused && audio.paused) {
  202. video.play();
  203. audio.play();
  204. } else {
  205. video.pause();
  206. audio.pause();
  207. }
  208. });
  209. buttonDiv.appendChild(playPauseButton);
  210.  
  211. const resetButton = document.createElement("button");
  212. resetButton.textContent = "Reset";
  213. resetButton.addEventListener("click", () => {
  214. video.currentTime = 0;
  215. audio.currentTime = 0;
  216. });
  217. buttonDiv.appendChild(resetButton);
  218.  
  219. let lastVideoTime = 0;
  220. // Sync audio with video on timeupdate event only if the difference is 2 seconds or more
  221. video.addEventListener("timeupdate", () => {
  222. if (Math.abs(video.currentTime - lastVideoTime) >= 2) {
  223. audio.currentTime = video.currentTime;
  224. lastVideoTime = video.currentTime;
  225. }
  226. lastVideoTime = video.currentTime;
  227. });
  228. }
  229.  
  230. const cellButton = document.createElement("button");
  231. cellButton.textContent = "View Post";
  232. setStyles(cellButton, {
  233. backgroundColor: "var(--main-color)",
  234. color: "var(--text-color)",
  235. });
  236. cellButton.addEventListener("click", () => {
  237. post.scrollIntoView({ behavior: "smooth" });
  238. gallerySize = {
  239. width: gridContainer.offsetWidth,
  240. height: gridContainer.offsetHeight,
  241. };
  242. document.body.removeChild(gallery);
  243. });
  244.  
  245. buttonDiv.appendChild(cellButton);
  246. cell.appendChild(videoContainer);
  247. cell.appendChild(buttonDiv);
  248. } else {
  249. const image = document.createElement("img");
  250. image.src = mediaLink.href;
  251. setStyles(image, {
  252. maxWidth: "100%",
  253. maxHeight: "200px",
  254. objectFit: "contain",
  255. cursor: "pointer",
  256. });
  257. image.addEventListener("click", () => {
  258. post.scrollIntoView({ behavior: "smooth" });
  259. gallerySize = {
  260. width: gridContainer.offsetWidth,
  261. height: gridContainer.offsetHeight,
  262. };
  263. document.body.removeChild(gallery);
  264. });
  265. image.title = comment.textContent;
  266.  
  267. if (soundLink) {
  268. const audio = document.createElement("audio");
  269. audio.src = decodeURIComponent(soundLink[1].startsWith("http") ? soundLink[1] : `https://${soundLink[1]}`);
  270.  
  271. const playPauseButton = document.createElement("button");
  272. playPauseButton.textContent = "Play/Pause";
  273. setStyles(playPauseButton, {
  274. position: "absolute",
  275. bottom: "10px",
  276. left: "10px",
  277. });
  278. playPauseButton.addEventListener("click", () => {
  279. if (audio.paused) {
  280. audio.play();
  281. } else {
  282. audio.pause();
  283. }
  284. });
  285. buttonDiv.appendChild(playPauseButton);
  286. }
  287.  
  288. cell.appendChild(image);
  289. cell.appendChild(buttonDiv);
  290. }
  291. gridContainer.appendChild(cell);
  292. }
  293. }
  294. });
  295.  
  296. // Store the current scroll position and grid container size when closing the gallery
  297. console.log(`Last scroll position: ${lastScrollPosition} px`);
  298. gridContainer.addEventListener("scroll", () => {
  299. lastScrollPosition = gridContainer.scrollTop;
  300. console.log(`Current scroll position: ${lastScrollPosition} px`);
  301. });
  302.  
  303. // Restore the last scroll position and grid container size when opening the gallery after a timeout if the url is the same
  304. if (window.location.href === threadURL) {
  305. setTimeout(() => {
  306. gridContainer.scrollTop = lastScrollPosition;
  307. console.log(`Restored scroll position: ${lastScrollPosition} px`);
  308. if (gallerySize.width > 0 && gallerySize.height > 0) {
  309. gridContainer.style.width = `${gallerySize.width}px`;
  310. gridContainer.style.height = `${gallerySize.height}px`;
  311. }
  312. }, 200);
  313. } else {
  314. // Reset the last scroll position and grid container size if the url is different
  315. threadURL = window.location.href;
  316. lastScrollPosition = 0;
  317. gallerySize = { width: 0, height: 0 };
  318. }
  319. };
  320.  
  321. loadPosts(mode); // Load posts based on the initial mode
  322.  
  323. gallery.appendChild(gridContainer);
  324.  
  325. const closeButton = document.createElement("button");
  326. closeButton.textContent = "Close";
  327. setStyles(closeButton, {
  328. position: "absolute",
  329. top: "10px",
  330. right: "10px",
  331. zIndex: "10000",
  332. backgroundColor: "var(--main-color)",
  333. color: "var(--text-color)",
  334. });
  335. closeButton.addEventListener("click", () => {
  336. gallerySize = {
  337. width: gridContainer.offsetWidth,
  338. height: gridContainer.offsetHeight,
  339. };
  340. document.body.removeChild(gallery);
  341. });
  342. gallery.appendChild(closeButton);
  343.  
  344. document.body.appendChild(gallery);
  345. };
  346.  
  347. button.addEventListener("click", openImageGallery);
  348. } else {
  349. // If there are less than two posts, try again after 5 seconds
  350. setTimeout(loadButton, 5000);
  351. }
  352. };
  353.  
  354. loadButton();
  355. console.log("4chan Gallery loaded successfully!");
  356. })();