8chan Lightweight Extended Suite

Minimalist extender for 8chan with in-line replies, spoiler revealing and media preview for videos & images

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

  1. // ==UserScript==
  2. // @name 8chan Lightweight Extended Suite
  3. // @namespace https://greasyfork.org/en/scripts/533173
  4. // @version 2.2.6
  5. // @description Minimalist extender for 8chan with in-line replies, spoiler revealing and media preview for videos & images
  6. // @author impregnator
  7. // @match https://8chan.moe/*
  8. // @match https://8chan.se/*
  9. // @match https://8chan.cc/*
  10. // @grant none
  11. // @license MIT
  12. // ==/UserScript==
  13.  
  14. (function() {
  15. 'use strict';
  16.  
  17. // Function to process images and replace spoiler placeholders with thumbnails
  18. function processImages(images, isCatalog = false) {
  19. images.forEach(img => {
  20. // Check if the image is a spoiler placeholder (custom or default)
  21. if (img.src.includes('custom.spoiler') || img.src.includes('spoiler.png')) {
  22. let fullFileUrl;
  23. if (isCatalog) {
  24. // Catalog: Get the href from the parent <a class="linkThumb">
  25. const link = img.closest('a.linkThumb');
  26. if (link) {
  27. // Construct the thumbnail URL based on the thread URL
  28. fullFileUrl = link.href;
  29. const threadMatch = fullFileUrl.match(/\/([a-z0-9]+)\/res\/([0-9]+)\.html$/i);
  30. if (threadMatch && threadMatch[1] && threadMatch[2]) {
  31. const board = threadMatch[1];
  32. const threadId = threadMatch[2];
  33. // Fetch the thread page to find the actual image URL
  34. fetchThreadImage(board, threadId).then(thumbnailUrl => {
  35. if (thumbnailUrl) {
  36. img.src = thumbnailUrl;
  37. }
  38. });
  39. }
  40. }
  41. } else {
  42. // Thread: Get the parent <a> element containing the full-sized file URL
  43. const link = img.closest('a.imgLink');
  44. if (link) {
  45. // Extract the full-sized file URL
  46. fullFileUrl = link.href;
  47. // Extract the file hash (everything after /.media/ up to the extension)
  48. const fileHash = fullFileUrl.match(/\/\.media\/([a-f0-9]+)\.[a-z0-9]+$/i);
  49. if (fileHash && fileHash[1]) {
  50. // Construct the thumbnail URL using the current domain
  51. const thumbnailUrl = `${window.location.origin}/.media/t_${fileHash[1]}`;
  52. // Replace the spoiler image with the thumbnail
  53. img.src = thumbnailUrl;
  54. }
  55. }
  56. }
  57. }
  58. });
  59. }
  60.  
  61. // Function to fetch the thread page and extract the thumbnail URL
  62. async function fetchThreadImage(board, threadId) {
  63. try {
  64. const response = await fetch(`https://${window.location.host}/${board}/res/${threadId}.html`);
  65. const text = await response.text();
  66. const parser = new DOMParser();
  67. const doc = parser.parseFromString(text, 'text/html');
  68. // Find the first image in the thread's OP post
  69. const imgLink = doc.querySelector('.uploadCell a.imgLink');
  70. if (imgLink) {
  71. const fullFileUrl = imgLink.href;
  72. const fileHash = fullFileUrl.match(/\/\.media\/([a-f0-9]+)\.[a-z0-9]+$/i);
  73. if (fileHash && fileHash[1]) {
  74. return `${window.location.origin}/.media/t_${fileHash[1]}`;
  75. }
  76. }
  77. return null;
  78. } catch (error) {
  79. console.error('Error fetching thread image:', error);
  80. return null;
  81. }
  82. }
  83.  
  84. // Process existing images on page load
  85. const isCatalogPage = window.location.pathname.includes('catalog.html');
  86. if (isCatalogPage) {
  87. const initialCatalogImages = document.querySelectorAll('.catalogCell a.linkThumb img');
  88. processImages(initialCatalogImages, true);
  89. } else {
  90. const initialThreadImages = document.querySelectorAll('.uploadCell img');
  91. processImages(initialThreadImages, false);
  92. }
  93.  
  94. // Set up MutationObserver to handle dynamically added posts
  95. const observer = new MutationObserver(mutations => {
  96. mutations.forEach(mutation => {
  97. if (mutation.addedNodes.length) {
  98. // Check each added node for new images
  99. mutation.addedNodes.forEach(node => {
  100. if (node.nodeType === Node.ELEMENT_NODE) {
  101. if (isCatalogPage) {
  102. const newCatalogImages = node.querySelectorAll('.catalogCell a.linkThumb img');
  103. processImages(newCatalogImages, true);
  104. } else {
  105. const newThreadImages = node.querySelectorAll('.uploadCell img');
  106. processImages(newThreadImages, false);
  107. }
  108. }
  109. });
  110. }
  111. });
  112. });
  113.  
  114. // Observe changes to the document body, including child nodes and subtrees
  115. observer.observe(document.body, {
  116. childList: true,
  117. subtree: true
  118. });
  119. })();
  120.  
  121. //Opening all posts from the catalog in a new tag section
  122.  
  123. // Add click event listener to catalog thumbnail images
  124. document.addEventListener('click', function(e) {
  125. // Check if the clicked element is an image inside a catalog cell
  126. if (e.target.tagName === 'IMG' && e.target.closest('.catalogCell')) {
  127. // Find the parent link with class 'linkThumb'
  128. const link = e.target.closest('.linkThumb');
  129. if (link) {
  130. // Prevent default link behavior
  131. e.preventDefault();
  132. // Open the thread in a new tab
  133. window.open(link.href, '_blank');
  134. }
  135. }
  136. });
  137.  
  138. //Automatically redirect to catalog section
  139.  
  140. // Redirect to catalog if on a board's main page, excluding overboard pages
  141. (function() {
  142. const currentPath = window.location.pathname;
  143. // Check if the path matches a board's main page (e.g., /v/, /a/) but not overboard pages
  144. if (currentPath.match(/^\/[a-zA-Z0-9]+\/$/) && !currentPath.match(/^\/(sfw|overboard)\/$/)) {
  145. // Redirect to the catalog page
  146. window.location.replace(currentPath + 'catalog.html');
  147. }
  148. })();
  149.  
  150. // Text spoiler revealer
  151.  
  152. (function() {
  153. // Function to reveal spoilers
  154. function revealSpoilers() {
  155. const spoilers = document.querySelectorAll('span.spoiler');
  156. spoilers.forEach(spoiler => {
  157. // Override default spoiler styles to make text visible
  158. spoiler.style.background = 'none';
  159. spoiler.style.color = 'inherit';
  160. spoiler.style.textShadow = 'none';
  161. });
  162. }
  163.  
  164. // Run initially for existing spoilers
  165. revealSpoilers();
  166.  
  167. // Set up MutationObserver to watch for new spoilers
  168. const observer = new MutationObserver((mutations) => {
  169. mutations.forEach(mutation => {
  170. if (mutation.addedNodes.length > 0) {
  171. // Check if new nodes contain spoilers
  172. mutation.addedNodes.forEach(node => {
  173. if (node.nodeType === Node.ELEMENT_NODE) {
  174. const newSpoilers = node.querySelectorAll('span.spoiler');
  175. newSpoilers.forEach(spoiler => {
  176. spoiler.style.background = 'none';
  177. spoiler.style.color = 'inherit';
  178. spoiler.style.textShadow = 'none';
  179. });
  180. }
  181. });
  182. }
  183. });
  184. });
  185.  
  186. // Observe the document body for changes (new posts)
  187. observer.observe(document.body, {
  188. childList: true,
  189. subtree: true
  190. });
  191. })();
  192.  
  193. //Hash navigation
  194. // Add # links to backlinks and quote links for scrolling
  195. (function() {
  196. // Function to add # link to backlinks and quote links
  197. function addHashLinks(container = document) {
  198. const links = container.querySelectorAll('.panelBacklinks a, .altBacklinks a, .divMessage .quoteLink');
  199. links.forEach(link => {
  200. // Skip if # link already exists or processed
  201. if (link.nextSibling && link.nextSibling.classList && link.nextSibling.classList.contains('hash-link-container')) return;
  202. if (link.dataset.hashProcessed) return;
  203. // Create # link as a span to avoid <a> processing
  204. const hashLink = document.createElement('span');
  205. hashLink.textContent = ' #';
  206. hashLink.style.cursor = 'pointer';
  207. hashLink.style.color = '#0000EE'; // Match link color
  208. hashLink.title = 'Scroll to post';
  209. hashLink.className = 'hash-link';
  210. hashLink.dataset.hashListener = 'true'; // Mark as processed
  211. // Wrap # link in a span to isolate it
  212. const container = document.createElement('span');
  213. container.className = 'hash-link-container';
  214. container.appendChild(hashLink);
  215. link.insertAdjacentElement('afterend', container);
  216. link.dataset.hashProcessed = 'true'; // Mark as processed
  217. });
  218. }
  219.  
  220. // Event delegation for hash link clicks to mimic .linkSelf behavior
  221. document.addEventListener('click', function(e) {
  222. if (e.target.classList.contains('hash-link')) {
  223. e.preventDefault();
  224. e.stopPropagation();
  225. const link = e.target.closest('.hash-link-container').previousElementSibling;
  226. const postId = link.textContent.replace('>>', '');
  227. if (document.getElementById(postId)) {
  228. window.location.hash = `#${postId}`;
  229. console.log(`Navigated to post #${postId}`);
  230. } else {
  231. console.log(`Post ${postId} not found`);
  232. }
  233. }
  234. }, true);
  235.  
  236. // Process existing backlinks and quote links on page load
  237. addHashLinks();
  238. console.log('Hash links applied on page load');
  239.  
  240. // Patch inline reply logic to apply hash links to new inline content
  241. if (window.tooltips) {
  242. // Patch loadTooltip to apply hash links after content is loaded
  243. const originalLoadTooltip = tooltips.loadTooltip;
  244. tooltips.loadTooltip = function(element, quoteUrl, sourceId, isInline) {
  245. originalLoadTooltip.apply(this, arguments);
  246. if (isInline) {
  247. // Wait for content to be fully loaded
  248. setTimeout(() => {
  249. addHashLinks(element);
  250. console.log('Hash links applied to loaded tooltip content:', quoteUrl);
  251. }, 0);
  252. }
  253. };
  254.  
  255. // Patch addLoadedTooltip to ensure hash links are applied
  256. const originalAddLoadedTooltip = tooltips.addLoadedTooltip;
  257. tooltips.addLoadedTooltip = function(htmlContents, tooltip, quoteUrl, replyId, isInline) {
  258. originalAddLoadedTooltip.apply(this, arguments);
  259. if (isInline) {
  260. addHashLinks(htmlContents);
  261. console.log('Hash links applied to inline tooltip content:', quoteUrl);
  262. }
  263. };
  264.  
  265. // Patch addInlineClick to apply hash links after appending
  266. const originalAddInlineClick = tooltips.addInlineClick;
  267. tooltips.addInlineClick = function(quote, innerPost, isBacklink, quoteTarget, sourceId) {
  268. if (!quote.href || quote.classList.contains('hash-link') || quote.closest('.hash-link-container') || quote.href.includes('#q')) {
  269. console.log('Skipped invalid or hash link:', quote.href || quote.textContent);
  270. return;
  271. }
  272. // Clone quote to remove existing listeners
  273. const newQuote = quote.cloneNode(true);
  274. quote.parentNode.replaceChild(newQuote, quote);
  275. quote = newQuote;
  276.  
  277. // Reapply hover events
  278. tooltips.addHoverEvents(quote, innerPost, quoteTarget, sourceId);
  279. console.log('Hover events reapplied for:', quoteTarget.quoteUrl);
  280.  
  281. // Add click handler
  282. quote.addEventListener('click', function(e) {
  283. console.log('linkQuote clicked:', quoteTarget.quoteUrl);
  284. if (!tooltips.inlineReplies) {
  285. console.log('inlineReplies disabled');
  286. return;
  287. }
  288. e.preventDefault();
  289. e.stopPropagation();
  290.  
  291. // Find or create replyPreview
  292. let replyPreview = innerPost.querySelector('.replyPreview');
  293. if (!replyPreview) {
  294. replyPreview = document.createElement('div');
  295. replyPreview.className = 'replyPreview';
  296. innerPost.appendChild(replyPreview);
  297. }
  298.  
  299. // Check for duplicates or loading
  300. if (tooltips.loadingPreviews[quoteTarget.quoteUrl] ||
  301. tooltips.quoteAlreadyAdded(quoteTarget.quoteUrl, innerPost)) {
  302. console.log('Duplicate or loading:', quoteTarget.quoteUrl);
  303. return;
  304. }
  305.  
  306. // Create and load inline post
  307. const placeHolder = document.createElement('div');
  308. placeHolder.style.whiteSpace = 'normal';
  309. placeHolder.className = 'inlineQuote';
  310. tooltips.loadTooltip(placeHolder, quoteTarget.quoteUrl, sourceId, true);
  311.  
  312. // Verify post loaded
  313. if (!placeHolder.querySelector('.linkSelf')) {
  314. console.log('Failed to load post:', quoteTarget.quoteUrl);
  315. return;
  316. }
  317.  
  318. // Add close button
  319. const close = document.createElement('a');
  320. close.innerText = 'X';
  321. close.className = 'closeInline';
  322. close.onclick = () => placeHolder.remove();
  323. placeHolder.querySelector('.postInfo').prepend(close);
  324.  
  325. // Process quotes in the new inline post
  326. Array.from(placeHolder.querySelectorAll('.linkQuote'))
  327. .forEach(a => tooltips.processQuote(a, false, true));
  328.  
  329. if (tooltips.bottomBacklinks) {
  330. const alts = placeHolder.querySelector('.altBacklinks');
  331. if (alts && alts.firstChild) {
  332. Array.from(alts.firstChild.children)
  333. .forEach(a => tooltips.processQuote(a, true));
  334. }
  335. }
  336.  
  337. // Append to replyPreview and apply hash links
  338. replyPreview.appendChild(placeHolder);
  339. addHashLinks(placeHolder);
  340. console.log('Inline post appended and hash links applied:', quoteTarget.quoteUrl);
  341.  
  342. tooltips.removeIfExists();
  343. }, true);
  344. };
  345.  
  346. // Patch processQuote to skip hash links
  347. const originalProcessQuote = tooltips.processQuote;
  348. tooltips.processQuote = function(quote, isBacklink) {
  349. if (!quote.href || quote.classList.contains('hash-link') || quote.closest('.hash-link-container') || quote.href.includes('#q')) {
  350. console.log('Skipped invalid or hash link in processQuote:', quote.href || quote.textContent);
  351. return;
  352. }
  353. originalProcessQuote.apply(this, arguments);
  354. };
  355. }
  356.  
  357. // Set up MutationObserver to handle dynamically added or updated backlinks and quote links
  358. const observer = new MutationObserver(mutations => {
  359. mutations.forEach(mutation => {
  360. if (mutation.addedNodes.length) {
  361. mutation.addedNodes.forEach(node => {
  362. if (node.nodeType === Node.ELEMENT_NODE) {
  363. // Check for new backlink or quote link <a> elements
  364. const newLinks = node.matches('.panelBacklinks a, .altBacklinks a, .divMessage .quoteLink') ? [node] : node.querySelectorAll('.panelBacklinks a, .altBacklinks a, .divMessage .quoteLink');
  365. newLinks.forEach(link => {
  366. addHashLinks(link.parentElement);
  367. console.log('Hash links applied to new link:', link.textContent);
  368. });
  369. }
  370. });
  371. }
  372. });
  373. });
  374.  
  375. // Observe changes to the posts container
  376. const postsContainer = document.querySelector('.divPosts') || document.body;
  377. observer.observe(postsContainer, {
  378. childList: true,
  379. subtree: true
  380. });
  381. })();
  382. //--Hash navigation
  383.  
  384. //Inline reply chains
  385.  
  386. (function() {
  387. 'use strict';
  388.  
  389. console.log('Userscript is running');
  390.  
  391. // Add CSS for visual nesting
  392. const style = document.createElement('style');
  393. style.innerHTML = `
  394. .inlineQuote .replyPreview {
  395. margin-left: 20px;
  396. border-left: 1px solid #ccc;
  397. padding-left: 10px;
  398. }
  399. .closeInline {
  400. color: #ff0000;
  401. cursor: pointer;
  402. margin-left: 5px;
  403. font-weight: bold;
  404. }
  405. `;
  406. document.head.appendChild(style);
  407.  
  408. // Wait for tooltips to initialize
  409. window.addEventListener('load', function() {
  410. if (!window.tooltips) {
  411. console.error('tooltips module not found');
  412. return;
  413. }
  414. console.log('tooltips module found');
  415.  
  416. // Ensure Inline Replies is enabled
  417. if (!tooltips.inlineReplies) {
  418. console.log('Enabling Inline Replies');
  419. localStorage.setItem('inlineReplies', 'true');
  420. tooltips.inlineReplies = true;
  421.  
  422. // Check and update the checkbox, retrying if not yet loaded
  423. const enableCheckbox = () => {
  424. const inlineCheckbox = document.getElementById('settings-SW5saW5lIFJlcGxpZX');
  425. if (inlineCheckbox) {
  426. inlineCheckbox.checked = true;
  427. console.log('Inline Replies checkbox checked');
  428. return true;
  429. }
  430. console.warn('Inline Replies checkbox not found, retrying...');
  431. return false;
  432. };
  433.  
  434. // Try immediately
  435. if (!enableCheckbox()) {
  436. // Retry every 500ms up to 5 seconds
  437. let attempts = 0;
  438. const maxAttempts = 10;
  439. const interval = setInterval(() => {
  440. if (enableCheckbox() || attempts >= maxAttempts) {
  441. clearInterval(interval);
  442. if (attempts >= maxAttempts) {
  443. console.error('Failed to find Inline Replies checkbox after retries');
  444. }
  445. }
  446. attempts++;
  447. }, 500);
  448. }
  449. } else {
  450. console.log('Inline Replies already enabled');
  451. }
  452.  
  453. // Override addLoadedTooltip to ensure replyPreview exists
  454. const originalAddLoadedTooltip = tooltips.addLoadedTooltip;
  455. tooltips.addLoadedTooltip = function(htmlContents, tooltip, quoteUrl, replyId, isInline) {
  456. console.log('addLoadedTooltip called for:', quoteUrl);
  457. originalAddLoadedTooltip.apply(this, arguments);
  458. if (isInline) {
  459. let replyPreview = htmlContents.querySelector('.replyPreview');
  460. if (!replyPreview) {
  461. replyPreview = document.createElement('div');
  462. replyPreview.className = 'replyPreview';
  463. htmlContents.appendChild(replyPreview);
  464. }
  465. }
  466. };
  467.  
  468. // Override addInlineClick for nested replies, excluding post number links
  469. tooltips.addInlineClick = function(quote, innerPost, isBacklink, quoteTarget, sourceId) {
  470. // Skip post number links (href starts with #q)
  471. if (quote.href.includes('#q')) {
  472. console.log('Skipping post number link:', quote.href);
  473. return;
  474. }
  475.  
  476. // Remove existing listeners by cloning
  477. const newQuote = quote.cloneNode(true);
  478. quote.parentNode.replaceChild(newQuote, quote);
  479. quote = newQuote;
  480.  
  481. // Reapply hover events to preserve preview functionality
  482. tooltips.addHoverEvents(quote, innerPost, quoteTarget, sourceId);
  483. console.log('Hover events reapplied for:', quoteTarget.quoteUrl);
  484.  
  485. // Add click handler
  486. quote.addEventListener('click', function(e) {
  487. console.log('linkQuote clicked:', quoteTarget.quoteUrl);
  488. if (!tooltips.inlineReplies) {
  489. console.log('inlineReplies disabled');
  490. return;
  491. }
  492. e.preventDefault();
  493. e.stopPropagation(); // Prevent site handlers
  494.  
  495. // Find or create replyPreview
  496. let replyPreview = innerPost.querySelector('.replyPreview');
  497. if (!replyPreview) {
  498. replyPreview = document.createElement('div');
  499. replyPreview.className = 'replyPreview';
  500. innerPost.appendChild(replyPreview);
  501. }
  502.  
  503. // Check for duplicates or loading
  504. if (tooltips.loadingPreviews[quoteTarget.quoteUrl] ||
  505. tooltips.quoteAlreadyAdded(quoteTarget.quoteUrl, innerPost)) {
  506. console.log('Duplicate or loading:', quoteTarget.quoteUrl);
  507. return;
  508. }
  509.  
  510. // Create and load inline post
  511. const placeHolder = document.createElement('div');
  512. placeHolder.style.whiteSpace = 'normal';
  513. placeHolder.className = 'inlineQuote';
  514. tooltips.loadTooltip(placeHolder, quoteTarget.quoteUrl, sourceId, true);
  515.  
  516. // Verify post loaded
  517. if (!placeHolder.querySelector('.linkSelf')) {
  518. console.log('Failed to load post:', quoteTarget.quoteUrl);
  519. return;
  520. }
  521.  
  522. // Add close button
  523. const close = document.createElement('a');
  524. close.innerText = 'X';
  525. close.className = 'closeInline';
  526. close.onclick = () => placeHolder.remove();
  527. placeHolder.querySelector('.postInfo').prepend(close);
  528.  
  529. // Process quotes in the new inline post
  530. Array.from(placeHolder.querySelectorAll('.linkQuote'))
  531. .forEach(a => tooltips.processQuote(a, false, true));
  532.  
  533. if (tooltips.bottomBacklinks) {
  534. const alts = placeHolder.querySelector('.altBacklinks');
  535. if (alts && alts.firstChild) {
  536. Array.from(alts.firstChild.children)
  537. .forEach(a => tooltips.processQuote(a, true));
  538. }
  539. }
  540.  
  541. // Append to replyPreview
  542. replyPreview.appendChild(placeHolder);
  543. console.log('Inline post appended:', quoteTarget.quoteUrl);
  544.  
  545. tooltips.removeIfExists();
  546. }, true); // Use capture phase
  547. };
  548.  
  549. // Reprocess all existing linkQuote and backlink elements, excluding post numbers
  550. console.log('Reprocessing linkQuote elements');
  551. const quotes = document.querySelectorAll('.linkQuote, .panelBacklinks a');
  552. quotes.forEach(quote => {
  553. const innerPost = quote.closest('.innerPost, .innerOP');
  554. if (!innerPost) {
  555. console.log('No innerPost found for quote:', quote.href);
  556. return;
  557. }
  558.  
  559. // Skip post number links
  560. if (quote.href.includes('#q')) {
  561. console.log('Skipping post number link:', quote.href);
  562. return;
  563. }
  564.  
  565. const isBacklink = quote.parentElement.classList.contains('panelBacklinks') ||
  566. quote.parentElement.classList.contains('altBacklinks');
  567. const quoteTarget = api.parsePostLink(quote.href);
  568. const sourceId = api.parsePostLink(innerPost.querySelector('.linkSelf').href).post;
  569.  
  570. tooltips.addInlineClick(quote, innerPost, isBacklink, quoteTarget, sourceId);
  571. });
  572.  
  573. // Observe for dynamically added posts
  574. const observer = new MutationObserver(mutations => {
  575. mutations.forEach(mutation => {
  576. mutation.addedNodes.forEach(node => {
  577. if (node.nodeType !== 1) return;
  578. const newQuotes = node.querySelectorAll('.linkQuote, .panelBacklinks a');
  579. newQuotes.forEach(quote => {
  580. if (quote.dataset.processed || quote.href.includes('#q')) {
  581. if (quote.href.includes('#q')) {
  582. console.log('Skipping post number link:', quote.href);
  583. }
  584. return;
  585. }
  586. quote.dataset.processed = 'true';
  587. const innerPost = quote.closest('.innerPost, .innerOP');
  588. if (!innerPost) return;
  589.  
  590. const isBacklink = quote.parentElement.classList.contains('panelBacklinks') ||
  591. quote.parentElement.classList.contains('altBacklinks');
  592. const quoteTarget = api.parsePostLink(quote.href);
  593. const sourceId = api.parsePostLink(innerPost.querySelector('.linkSelf').href).post;
  594.  
  595. tooltips.addInlineClick(quote, innerPost, isBacklink, quoteTarget, sourceId);
  596. });
  597. });
  598. });
  599. });
  600. observer.observe(document.querySelector('.divPosts') || document.body, {
  601. childList: true,
  602. subtree: true
  603. });
  604. console.log('MutationObserver set up');
  605. });
  606. })();
  607.  
  608. //--Inline replies
  609.  
  610. //Auto TOS accept
  611. (function() {
  612. 'use strict';
  613.  
  614. // Check if on the disclaimer page
  615. if (window.location.pathname === '/.static/pages/disclaimer.html') {
  616. // Redirect to confirmed page
  617. window.location.replace('https://8chan.se/.static/pages/confirmed.html');
  618. console.log('Automatically redirected from disclaimer to confirmed page');
  619. }
  620. })();
  621. //--Auto TOS accept
  622.  
  623. //Media Auto-Preview
  624. // Auto-preview images and videos on hover for un-expanded thumbnails, disabling native hover
  625. (function() {
  626. 'use strict';
  627.  
  628. // Disable native hover preview
  629. localStorage.setItem('hoveringImage', 'false'); // Disable "Image Preview on Hover" setting
  630. if (window.thumbs && typeof window.thumbs.removeHoveringExpand === 'function') {
  631. window.thumbs.removeHoveringExpand(); // Remove native hover listeners
  632. }
  633. // Override addHoveringExpand to prevent re-enabling
  634. if (window.thumbs) {
  635. window.thumbs.addHoveringExpand = function() {
  636. // Do nothing to prevent native hover preview
  637. console.log('Native hover preview (addHoveringExpand) blocked by userscript');
  638. };
  639. }
  640.  
  641. // Supported file extensions for images and videos
  642. const supportedExtensions = {
  643. image: ['.gif', '.webp', '.png', '.jfif', '.pjpeg', '.jpeg', '.pjp', '.jpg', '.bmp', '.dib', '.svgz', '.svg'],
  644. video: ['.webm', '.m4v', '.mp4', '.ogm', '.ogv', '.avi', '.asx', '.mpg', '.mpeg']
  645. };
  646.  
  647. // Create preview container
  648. const previewContainer = document.createElement('div');
  649. previewContainer.style.position = 'fixed';
  650. previewContainer.style.zIndex = '1000';
  651. previewContainer.style.pointerEvents = 'none'; // Allow clicks to pass through
  652. previewContainer.style.display = 'none';
  653. document.body.appendChild(previewContainer);
  654.  
  655. // Function to check if URL is a supported image or video
  656. function isSupportedMedia(url) {
  657. const ext = (url.match(/\.[a-z0-9]+$/i) || [''])[0].toLowerCase();
  658. return supportedExtensions.image.includes(ext) || supportedExtensions.video.includes(ext);
  659. }
  660.  
  661. // Function to check if URL is a video
  662. function isVideo(url) {
  663. const ext = (url.match(/\.[a-z0-9]+$/i) || [''])[0].toLowerCase();
  664. return supportedExtensions.video.includes(ext);
  665. }
  666.  
  667. // Function to check if link is in un-expanded state
  668. function isUnexpanded(link) {
  669. const thumbnail = link.querySelector('img:not(.imgExpanded)');
  670. const expanded = link.querySelector('img.imgExpanded');
  671. return thumbnail && window.getComputedStyle(thumbnail).display !== 'none' &&
  672. (!expanded || window.getComputedStyle(expanded).display === 'none');
  673. }
  674.  
  675. // Function to calculate preview dimensions
  676. function getPreviewDimensions(naturalWidth, naturalHeight) {
  677. const maxWidth = window.innerWidth; // 100% of viewport width
  678. const maxHeight = window.innerHeight; // 100% of viewport height
  679.  
  680. // If image is smaller than or equal to viewport, use native size
  681. if (naturalWidth <= maxWidth && naturalHeight <= maxHeight) {
  682. return { width: naturalWidth, height: naturalHeight };
  683. }
  684.  
  685. // Otherwise, scale to fit viewport
  686. // Calculate scaling factors to fit width and height
  687. const scaleByWidth = maxWidth / naturalWidth;
  688. const scaleByHeight = maxHeight / naturalHeight;
  689. // Use the smaller scale to ensure the entire media fits
  690. const scale = Math.min(scaleByWidth, scaleByHeight);
  691.  
  692. // Scale both dimensions by the same factor
  693. const width = Math.round(naturalWidth * scale);
  694. const height = Math.round(naturalHeight * scale);
  695.  
  696. return { width, height };
  697. }
  698.  
  699. // Function to position preview near cursor
  700. function positionPreview(event) {
  701. const mouseX = event.clientX;
  702. const mouseY = event.clientY;
  703. const previewWidth = previewContainer.offsetWidth;
  704. const previewHeight = previewContainer.offsetHeight;
  705.  
  706. // Calculate centered position
  707. const centerX = (window.innerWidth - previewWidth) / 2;
  708. const centerY = (window.innerHeight - previewHeight) / 2;
  709.  
  710. // Allow cursor to influence position with a bounded offset
  711. const maxOffset = 100; // Maximum pixels to shift from center
  712. const cursorOffsetX = Math.max(-maxOffset, Math.min(maxOffset, mouseX - window.innerWidth / 2));
  713. const cursorOffsetY = Math.max(-maxOffset, Math.min(maxOffset, mouseY - window.innerHeight / 2));
  714.  
  715. // Calculate initial position with cursor influence
  716. let left = centerX + cursorOffsetX;
  717. let top = centerY + cursorOffsetY;
  718.  
  719. // Ensure preview stays fully on-screen
  720. left = Math.max(0, Math.min(left, window.innerWidth - previewWidth));
  721. top = Math.max(0, Math.min(top, window.innerHeight - previewHeight));
  722.  
  723. previewContainer.style.left = `${left}px`;
  724. previewContainer.style.top = `${top}px`;
  725. }
  726.  
  727. // Function to show preview
  728. function showPreview(link, event) {
  729. if (!isUnexpanded(link)) return; // Skip if expanded
  730. const url = link.href;
  731. if (!isSupportedMedia(url)) return;
  732.  
  733. // Clear existing preview
  734. previewContainer.innerHTML = '';
  735.  
  736. if (isVideo(url)) {
  737. // Create video element
  738. const video = document.createElement('video');
  739. video.src = url;
  740. video.autoplay = true;
  741. video.muted = false; // Play with audio
  742. video.loop = true;
  743. video.style.maxWidth = '100%';
  744. video.style.maxHeight = '100%';
  745.  
  746. // Set dimensions when metadata is loaded
  747. video.onloadedmetadata = () => {
  748. const { width, height } = getPreviewDimensions(video.videoWidth, video.videoHeight);
  749. video.width = width;
  750. video.height = height;
  751. previewContainer.style.width = `${width}px`;
  752. previewContainer.style.height = `${height}px`;
  753. positionPreview(event);
  754. };
  755.  
  756. previewContainer.appendChild(video);
  757. } else {
  758. // Create image element
  759. const img = document.createElement('img');
  760. img.src = url;
  761. img.style.maxWidth = '100%';
  762. img.style.maxHeight = '100%';
  763.  
  764. // Set dimensions when image is loaded
  765. img.onload = () => {
  766. const { width, height } = getPreviewDimensions(img.naturalWidth, img.naturalHeight);
  767. img.width = width;
  768. img.height = height;
  769. previewContainer.style.width = `${width}px`;
  770. previewContainer.style.height = `${height}px`;
  771. positionPreview(event);
  772. };
  773.  
  774. previewContainer.appendChild(img);
  775. }
  776.  
  777. // Show preview
  778. previewContainer.style.display = 'block';
  779. positionPreview(event);
  780. }
  781.  
  782. // Function to hide preview
  783. function hidePreview() {
  784. previewContainer.style.display = 'none';
  785. // Stop video playback
  786. const video = previewContainer.querySelector('video');
  787. if (video) {
  788. video.pause();
  789. video.currentTime = 0;
  790. }
  791. previewContainer.innerHTML = '';
  792. }
  793.  
  794. // Function to apply hover events to links
  795. function applyHoverEvents(container = document) {
  796. const links = container.querySelectorAll('.uploadCell a.imgLink');
  797. links.forEach(link => {
  798. // Skip if already processed
  799. if (link.dataset.previewProcessed) return;
  800. link.dataset.previewProcessed = 'true';
  801.  
  802. link.addEventListener('mouseenter', (e) => {
  803. showPreview(link, e);
  804. });
  805.  
  806. link.addEventListener('mousemove', (e) => {
  807. if (previewContainer.style.display === 'block') {
  808. positionPreview(e);
  809. }
  810. });
  811.  
  812. link.addEventListener('mouseleave', () => {
  813. hidePreview();
  814. });
  815.  
  816. // Hide preview on click if expanded
  817. link.addEventListener('click', () => {
  818. if (!isUnexpanded(link)) {
  819. hidePreview();
  820. }
  821. });
  822. });
  823. }
  824.  
  825. // Apply hover events to existing links on page load
  826. applyHoverEvents();
  827. console.log('Media preview events applied on page load');
  828.  
  829. // Patch inline reply logic to apply hover events to new inline content
  830. if (window.tooltips) {
  831. // Patch loadTooltip to apply hover events after content is loaded
  832. const originalLoadTooltip = tooltips.loadTooltip;
  833. tooltips.loadTooltip = function(element, quoteUrl, sourceId, isInline) {
  834. originalLoadTooltip.apply(this, arguments);
  835. if (isInline) {
  836. setTimeout(() => {
  837. applyHoverEvents(element);
  838. console.log('Media preview events applied to loaded tooltip content:', quoteUrl);
  839. }, 0);
  840. }
  841. };
  842.  
  843. // Patch addLoadedTooltip to ensure hover events are applied
  844. const originalAddLoadedTooltip = tooltips.addLoadedTooltip;
  845. tooltips.addLoadedTooltip = function(htmlContents, tooltip, quoteUrl, replyId, isInline) {
  846. originalAddLoadedTooltip.apply(this, arguments);
  847. if (isInline) {
  848. applyHoverEvents(htmlContents);
  849. console.log('Media preview events applied to inline tooltip content:', quoteUrl);
  850. }
  851. };
  852. }
  853.  
  854. // Set up MutationObserver to handle dynamically added posts
  855. const observer = new MutationObserver(mutations => {
  856. mutations.forEach(mutation => {
  857. if (mutation.addedNodes.length) {
  858. mutation.addedNodes.forEach(node => {
  859. if (node.nodeType === Node.ELEMENT_NODE) {
  860. // Handle new posts and inline replies
  861. const newLinks = node.matches('.uploadCell a.imgLink') ? [node] : node.querySelectorAll('.uploadCell a.imgLink');
  862. newLinks.forEach(link => {
  863. applyHoverEvents(link.parentElement);
  864. console.log('Media preview events applied to new link:', link.href);
  865. });
  866. }
  867. });
  868. }
  869. });
  870. });
  871.  
  872. // Observe changes to the posts container
  873. const postsContainer = document.querySelector('.divPosts') || document.body;
  874. observer.observe(postsContainer, {
  875. childList: true,
  876. subtree: true
  877. });
  878. })();
  879. //--Media Auto-Preview