🏠 Home 

Greasy Fork is available in English.

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.


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