- // ==UserScript==
- // @name 8chan Lightweight Extended Suite
- // @namespace https://greatest.deepsurf.us/en/scripts/533173
- // @version 2.6.2
- // @description Minimalist extender for 8chan with in-line replies, spoiler revealing and media preview for videos & images
- // @author impregnator
- // @match https://8chan.moe/*
- // @match https://8chan.se/*
- // @match https://8chan.cc/*
- // @grant none
- // @license MIT
- // ==/UserScript==
-
- (function() {
- 'use strict';
-
- // Function to process images and replace spoiler placeholders with thumbnails
- function processImages(images, isCatalog = false) {
- images.forEach(img => {
- // Check if the image is a spoiler placeholder (custom or default)
- if (img.src.includes('custom.spoiler') || img.src.includes('spoiler.png')) {
- let fullFileUrl;
- if (isCatalog) {
- // Catalog: Get the href from the parent <a class="linkThumb">
- const link = img.closest('a.linkThumb');
- if (link) {
- // Construct the thumbnail URL based on the thread URL
- fullFileUrl = link.href;
- const threadMatch = fullFileUrl.match(/\/([a-z0-9]+)\/res\/([0-9]+)\.html$/i);
- if (threadMatch && threadMatch[1] && threadMatch[2]) {
- const board = threadMatch[1];
- const threadId = threadMatch[2];
- // Fetch the thread page to find the actual image URL
- fetchThreadImage(board, threadId).then(thumbnailUrl => {
- if (thumbnailUrl) {
- img.src = thumbnailUrl;
- }
- });
- }
- }
- } else {
- // Thread: Get the parent <a> element containing the full-sized file URL
- const link = img.closest('a.imgLink');
- if (link) {
- // Extract the full-sized file URL
- fullFileUrl = link.href;
- // Extract the file hash (everything after /.media/ up to the extension)
- const fileHash = fullFileUrl.match(/\/\.media\/([a-f0-9]+)\.[a-z0-9]+$/i);
- if (fileHash && fileHash[1]) {
- // Construct the thumbnail URL using the current domain
- const thumbnailUrl = `${window.location.origin}/.media/t_${fileHash[1]}`;
- // Replace the spoiler image with the thumbnail
- img.src = thumbnailUrl;
- }
- }
- }
- }
- });
- }
-
- // Function to fetch the thread page and extract the thumbnail URL
- async function fetchThreadImage(board, threadId) {
- try {
- const response = await fetch(`https://${window.location.host}/${board}/res/${threadId}.html`);
- const text = await response.text();
- const parser = new DOMParser();
- const doc = parser.parseFromString(text, 'text/html');
- // Find the first image in the thread's OP post
- const imgLink = doc.querySelector('.uploadCell a.imgLink');
- if (imgLink) {
- const fullFileUrl = imgLink.href;
- const fileHash = fullFileUrl.match(/\/\.media\/([a-f0-9]+)\.[a-z0-9]+$/i);
- if (fileHash && fileHash[1]) {
- return `${window.location.origin}/.media/t_${fileHash[1]}`;
- }
- }
- return null;
- } catch (error) {
- console.error('Error fetching thread image:', error);
- return null;
- }
- }
-
- // Process existing images on page load
- const isCatalogPage = window.location.pathname.includes('catalog.html');
- if (isCatalogPage) {
- const initialCatalogImages = document.querySelectorAll('.catalogCell a.linkThumb img');
- processImages(initialCatalogImages, true);
- } else {
- const initialThreadImages = document.querySelectorAll('.uploadCell img');
- processImages(initialThreadImages, false);
- }
-
- // Set up MutationObserver to handle dynamically added posts
- const observer = new MutationObserver(mutations => {
- mutations.forEach(mutation => {
- if (mutation.addedNodes.length) {
- // Check each added node for new images
- mutation.addedNodes.forEach(node => {
- if (node.nodeType === Node.ELEMENT_NODE) {
- if (isCatalogPage) {
- const newCatalogImages = node.querySelectorAll('.catalogCell a.linkThumb img');
- processImages(newCatalogImages, true);
- } else {
- const newThreadImages = node.querySelectorAll('.uploadCell img');
- processImages(newThreadImages, false);
- }
- }
- });
- }
- });
- });
-
- // Observe changes to the document body, including child nodes and subtrees
- observer.observe(document.body, {
- childList: true,
- subtree: true
- });
- })();
-
- //Opening all posts from the catalog in a new tag section
-
- // Add click event listener to catalog thumbnail images
- document.addEventListener('click', function(e) {
- // Check if the clicked element is an image inside a catalog cell
- if (e.target.tagName === 'IMG' && e.target.closest('.catalogCell')) {
- // Find the parent link with class 'linkThumb'
- const link = e.target.closest('.linkThumb');
- if (link) {
- // Prevent default link behavior
- e.preventDefault();
- // Open the thread in a new tab
- window.open(link.href, '_blank');
- }
- }
- });
-
- //Automatically redirect to catalog section
-
- // Redirect to catalog if on a board's main page, excluding overboard pages
- (function() {
- const currentPath = window.location.pathname;
- // Check if the path matches a board's main page (e.g., /v/, /a/) but not overboard pages
- if (currentPath.match(/^\/[a-zA-Z0-9]+\/$/) && !currentPath.match(/^\/(sfw|overboard)\/$/)) {
- // Redirect to the catalog page
- window.location.replace(currentPath + 'catalog.html');
- }
- })();
-
- // Text spoiler revealer
-
- (function() {
- // Function to reveal spoilers
- function revealSpoilers() {
- const spoilers = document.querySelectorAll('span.spoiler');
- spoilers.forEach(spoiler => {
- // Override default spoiler styles to make text visible
- spoiler.style.background = 'none';
- spoiler.style.color = 'inherit';
- spoiler.style.textShadow = 'none';
- });
- }
-
- // Run initially for existing spoilers
- revealSpoilers();
-
- // Set up MutationObserver to watch for new spoilers
- const observer = new MutationObserver((mutations) => {
- mutations.forEach(mutation => {
- if (mutation.addedNodes.length > 0) {
- // Check if new nodes contain spoilers
- mutation.addedNodes.forEach(node => {
- if (node.nodeType === Node.ELEMENT_NODE) {
- const newSpoilers = node.querySelectorAll('span.spoiler');
- newSpoilers.forEach(spoiler => {
- spoiler.style.background = 'none';
- spoiler.style.color = 'inherit';
- spoiler.style.textShadow = 'none';
- });
- }
- });
- }
- });
- });
-
- // Observe the document body for changes (new posts)
- observer.observe(document.body, {
- childList: true,
- subtree: true
- });
- })();
-
- //Hash navigation
- // Add # links to backlinks and quote links for scrolling
- (function() {
- // Function to add # link to backlinks and quote links
- function addHashLinks(container = document) {
- const links = container.querySelectorAll('.panelBacklinks a, .altBacklinks a, .divMessage .quoteLink');
- links.forEach(link => {
- // Skip if # link already exists or processed
- if (link.nextSibling && link.nextSibling.classList && link.nextSibling.classList.contains('hash-link-container')) return;
- if (link.dataset.hashProcessed) return;
- // Create # link as a span to avoid <a> processing
- const hashLink = document.createElement('span');
- hashLink.textContent = ' #';
- hashLink.style.cursor = 'pointer';
- hashLink.style.color = '#0000EE'; // Match link color
- hashLink.title = 'Scroll to post';
- hashLink.className = 'hash-link';
- hashLink.dataset.hashListener = 'true'; // Mark as processed
- // Wrap # link in a span to isolate it
- const container = document.createElement('span');
- container.className = 'hash-link-container';
- container.appendChild(hashLink);
- link.insertAdjacentElement('afterend', container);
- link.dataset.hashProcessed = 'true'; // Mark as processed
- });
- }
-
- // Event delegation for hash link clicks to mimic .linkSelf behavior
- document.addEventListener('click', function(e) {
- if (e.target.classList.contains('hash-link')) {
- e.preventDefault();
- e.stopPropagation();
- const link = e.target.closest('.hash-link-container').previousElementSibling;
- const postId = link.textContent.replace('>>', '');
- if (document.getElementById(postId)) {
- window.location.hash = `#${postId}`;
- console.log(`Navigated to post #${postId}`);
- } else {
- console.log(`Post ${postId} not found`);
- }
- }
- }, true);
-
- // Process existing backlinks and quote links on page load
- addHashLinks();
- console.log('Hash links applied on page load');
-
- // Patch inline reply logic to apply hash links to new inline content
- if (window.tooltips) {
- // Patch loadTooltip to apply hash links after content is loaded
- const originalLoadTooltip = tooltips.loadTooltip;
- tooltips.loadTooltip = function(element, quoteUrl, sourceId, isInline) {
- originalLoadTooltip.apply(this, arguments);
- if (isInline) {
- // Wait for content to be fully loaded
- setTimeout(() => {
- addHashLinks(element);
- console.log('Hash links applied to loaded tooltip content:', quoteUrl);
- }, 0);
- }
- };
-
- // Patch addLoadedTooltip to ensure hash links are applied
- const originalAddLoadedTooltip = tooltips.addLoadedTooltip;
- tooltips.addLoadedTooltip = function(htmlContents, tooltip, quoteUrl, replyId, isInline) {
- originalAddLoadedTooltip.apply(this, arguments);
- if (isInline) {
- addHashLinks(htmlContents);
- console.log('Hash links applied to inline tooltip content:', quoteUrl);
- }
- };
-
- // Patch addInlineClick to apply hash links after appending
- const originalAddInlineClick = tooltips.addInlineClick;
- tooltips.addInlineClick = function(quote, innerPost, isBacklink, quoteTarget, sourceId) {
- if (!quote.href || quote.classList.contains('hash-link') || quote.closest('.hash-link-container') || quote.href.includes('#q')) {
- console.log('Skipped invalid or hash link:', quote.href || quote.textContent);
- return;
- }
- // Clone quote to remove existing listeners
- const newQuote = quote.cloneNode(true);
- quote.parentNode.replaceChild(newQuote, quote);
- quote = newQuote;
-
- // Reapply hover events
- tooltips.addHoverEvents(quote, innerPost, quoteTarget, sourceId);
- console.log('Hover events reapplied for:', quoteTarget.quoteUrl);
-
- // Add click handler
- quote.addEventListener('click', function(e) {
- console.log('linkQuote clicked:', quoteTarget.quoteUrl);
- if (!tooltips.inlineReplies) {
- console.log('inlineReplies disabled');
- return;
- }
- e.preventDefault();
- e.stopPropagation();
-
- // Find or create replyPreview
- let replyPreview = innerPost.querySelector('.replyPreview');
- if (!replyPreview) {
- replyPreview = document.createElement('div');
- replyPreview.className = 'replyPreview';
- innerPost.appendChild(replyPreview);
- }
-
- // Check for duplicates or loading
- if (tooltips.loadingPreviews[quoteTarget.quoteUrl] ||
- tooltips.quoteAlreadyAdded(quoteTarget.quoteUrl, innerPost)) {
- console.log('Duplicate or loading:', quoteTarget.quoteUrl);
- return;
- }
-
- // Create and load inline post
- const placeHolder = document.createElement('div');
- placeHolder.style.whiteSpace = 'normal';
- placeHolder.className = 'inlineQuote';
- tooltips.loadTooltip(placeHolder, quoteTarget.quoteUrl, sourceId, true);
-
- // Verify post loaded
- if (!placeHolder.querySelector('.linkSelf')) {
- console.log('Failed to load post:', quoteTarget.quoteUrl);
- return;
- }
-
- // Add close button
- const close = document.createElement('a');
- close.innerText = 'X';
- close.className = 'closeInline';
- close.onclick = () => placeHolder.remove();
- placeHolder.querySelector('.postInfo').prepend(close);
-
- // Process quotes in the new inline post
- Array.from(placeHolder.querySelectorAll('.linkQuote'))
- .forEach(a => tooltips.processQuote(a, false, true));
-
- if (tooltips.bottomBacklinks) {
- const alts = placeHolder.querySelector('.altBacklinks');
- if (alts && alts.firstChild) {
- Array.from(alts.firstChild.children)
- .forEach(a => tooltips.processQuote(a, true));
- }
- }
-
- // Append to replyPreview and apply hash links
- replyPreview.appendChild(placeHolder);
- addHashLinks(placeHolder);
- console.log('Inline post appended and hash links applied:', quoteTarget.quoteUrl);
-
- tooltips.removeIfExists();
- }, true);
- };
-
- // Patch processQuote to skip hash links
- const originalProcessQuote = tooltips.processQuote;
- tooltips.processQuote = function(quote, isBacklink) {
- if (!quote.href || quote.classList.contains('hash-link') || quote.closest('.hash-link-container') || quote.href.includes('#q')) {
- console.log('Skipped invalid or hash link in processQuote:', quote.href || quote.textContent);
- return;
- }
- originalProcessQuote.apply(this, arguments);
- };
- }
-
- // Set up MutationObserver to handle dynamically added or updated backlinks and quote links
- const observer = new MutationObserver(mutations => {
- mutations.forEach(mutation => {
- if (mutation.addedNodes.length) {
- mutation.addedNodes.forEach(node => {
- if (node.nodeType === Node.ELEMENT_NODE) {
- // Check for new backlink or quote link <a> elements
- const newLinks = node.matches('.panelBacklinks a, .altBacklinks a, .divMessage .quoteLink') ? [node] : node.querySelectorAll('.panelBacklinks a, .altBacklinks a, .divMessage .quoteLink');
- newLinks.forEach(link => {
- addHashLinks(link.parentElement);
- console.log('Hash links applied to new link:', link.textContent);
- });
- }
- });
- }
- });
- });
-
- // Observe changes to the posts container
- const postsContainer = document.querySelector('.divPosts') || document.body;
- observer.observe(postsContainer, {
- childList: true,
- subtree: true
- });
- })();
- //--Hash navigation
-
- //Inline reply chains
-
- (function() {
- 'use strict';
-
- console.log('Userscript is running');
-
- // Add CSS for visual nesting
- const style = document.createElement('style');
- style.innerHTML = `
- .inlineQuote .replyPreview {
- margin-left: 20px;
- border-left: 1px solid #ccc;
- padding-left: 10px;
- }
- .closeInline {
- color: #ff0000;
- cursor: pointer;
- margin-left: 5px;
- font-weight: bold;
- }
- `;
- document.head.appendChild(style);
-
- // Wait for tooltips to initialize
- window.addEventListener('load', function() {
- if (!window.tooltips) {
- console.error('tooltips module not found');
- return;
- }
- console.log('tooltips module found');
-
- // Ensure Inline Replies is enabled
- if (!tooltips.inlineReplies) {
- console.log('Enabling Inline Replies');
- localStorage.setItem('inlineReplies', 'true');
- tooltips.inlineReplies = true;
-
- // Check and update the checkbox, retrying if not yet loaded
- const enableCheckbox = () => {
- const inlineCheckbox = document.getElementById('settings-SW5saW5lIFJlcGxpZX');
- if (inlineCheckbox) {
- inlineCheckbox.checked = true;
- console.log('Inline Replies checkbox checked');
- return true;
- }
- console.warn('Inline Replies checkbox not found, retrying...');
- return false;
- };
-
- // Try immediately
- if (!enableCheckbox()) {
- // Retry every 500ms up to 5 seconds
- let attempts = 0;
- const maxAttempts = 10;
- const interval = setInterval(() => {
- if (enableCheckbox() || attempts >= maxAttempts) {
- clearInterval(interval);
- if (attempts >= maxAttempts) {
- console.error('Failed to find Inline Replies checkbox after retries');
- }
- }
- attempts++;
- }, 500);
- }
- } else {
- console.log('Inline Replies already enabled');
- }
-
- // Override addLoadedTooltip to ensure replyPreview exists
- const originalAddLoadedTooltip = tooltips.addLoadedTooltip;
- tooltips.addLoadedTooltip = function(htmlContents, tooltip, quoteUrl, replyId, isInline) {
- console.log('addLoadedTooltip called for:', quoteUrl);
- originalAddLoadedTooltip.apply(this, arguments);
- if (isInline) {
- let replyPreview = htmlContents.querySelector('.replyPreview');
- if (!replyPreview) {
- replyPreview = document.createElement('div');
- replyPreview.className = 'replyPreview';
- htmlContents.appendChild(replyPreview);
- }
- }
- };
-
- // Override addInlineClick for nested replies, excluding post number links
- tooltips.addInlineClick = function(quote, innerPost, isBacklink, quoteTarget, sourceId) {
- // Skip post number links (href starts with #q)
- if (quote.href.includes('#q')) {
- console.log('Skipping post number link:', quote.href);
- return;
- }
-
- // Remove existing listeners by cloning
- const newQuote = quote.cloneNode(true);
- quote.parentNode.replaceChild(newQuote, quote);
- quote = newQuote;
-
- // Reapply hover events to preserve preview functionality
- tooltips.addHoverEvents(quote, innerPost, quoteTarget, sourceId);
- console.log('Hover events reapplied for:', quoteTarget.quoteUrl);
-
- // Add click handler
- quote.addEventListener('click', function(e) {
- console.log('linkQuote clicked:', quoteTarget.quoteUrl);
- if (!tooltips.inlineReplies) {
- console.log('inlineReplies disabled');
- return;
- }
- e.preventDefault();
- e.stopPropagation(); // Prevent site handlers
-
- // Find or create replyPreview
- let replyPreview = innerPost.querySelector('.replyPreview');
- if (!replyPreview) {
- replyPreview = document.createElement('div');
- replyPreview.className = 'replyPreview';
- innerPost.appendChild(replyPreview);
- }
-
- // Check for duplicates or loading
- if (tooltips.loadingPreviews[quoteTarget.quoteUrl] ||
- tooltips.quoteAlreadyAdded(quoteTarget.quoteUrl, innerPost)) {
- console.log('Duplicate or loading:', quoteTarget.quoteUrl);
- return;
- }
-
- // Create and load inline post
- const placeHolder = document.createElement('div');
- placeHolder.style.whiteSpace = 'normal';
- placeHolder.className = 'inlineQuote';
- tooltips.loadTooltip(placeHolder, quoteTarget.quoteUrl, sourceId, true);
-
- // Verify post loaded
- if (!placeHolder.querySelector('.linkSelf')) {
- console.log('Failed to load post:', quoteTarget.quoteUrl);
- return;
- }
-
- // Add close button
- const close = document.createElement('a');
- close.innerText = 'X';
- close.className = 'closeInline';
- close.onclick = () => placeHolder.remove();
- placeHolder.querySelector('.postInfo').prepend(close);
-
- // Process quotes in the new inline post
- Array.from(placeHolder.querySelectorAll('.linkQuote'))
- .forEach(a => tooltips.processQuote(a, false, true));
-
- if (tooltips.bottomBacklinks) {
- const alts = placeHolder.querySelector('.altBacklinks');
- if (alts && alts.firstChild) {
- Array.from(alts.firstChild.children)
- .forEach(a => tooltips.processQuote(a, true));
- }
- }
-
- // Append to replyPreview
- replyPreview.appendChild(placeHolder);
- console.log('Inline post appended:', quoteTarget.quoteUrl);
-
- tooltips.removeIfExists();
- }, true); // Use capture phase
- };
-
- // Reprocess all existing linkQuote and backlink elements, excluding post numbers
- console.log('Reprocessing linkQuote elements');
- const quotes = document.querySelectorAll('.linkQuote, .panelBacklinks a');
- quotes.forEach(quote => {
- const innerPost = quote.closest('.innerPost, .innerOP');
- if (!innerPost) {
- console.log('No innerPost found for quote:', quote.href);
- return;
- }
-
- // Skip post number links
- if (quote.href.includes('#q')) {
- console.log('Skipping post number link:', quote.href);
- return;
- }
-
- const isBacklink = quote.parentElement.classList.contains('panelBacklinks') ||
- quote.parentElement.classList.contains('altBacklinks');
- const quoteTarget = api.parsePostLink(quote.href);
- const sourceId = api.parsePostLink(innerPost.querySelector('.linkSelf').href).post;
-
- tooltips.addInlineClick(quote, innerPost, isBacklink, quoteTarget, sourceId);
- });
-
- // Observe for dynamically added posts
- const observer = new MutationObserver(mutations => {
- mutations.forEach(mutation => {
- mutation.addedNodes.forEach(node => {
- if (node.nodeType !== 1) return;
- const newQuotes = node.querySelectorAll('.linkQuote, .panelBacklinks a');
- newQuotes.forEach(quote => {
- if (quote.dataset.processed || quote.href.includes('#q')) {
- if (quote.href.includes('#q')) {
- console.log('Skipping post number link:', quote.href);
- }
- return;
- }
- quote.dataset.processed = 'true';
- const innerPost = quote.closest('.innerPost, .innerOP');
- if (!innerPost) return;
-
- const isBacklink = quote.parentElement.classList.contains('panelBacklinks') ||
- quote.parentElement.classList.contains('altBacklinks');
- const quoteTarget = api.parsePostLink(quote.href);
- const sourceId = api.parsePostLink(innerPost.querySelector('.linkSelf').href).post;
-
- tooltips.addInlineClick(quote, innerPost, isBacklink, quoteTarget, sourceId);
- });
- });
- });
- });
- observer.observe(document.querySelector('.divPosts') || document.body, {
- childList: true,
- subtree: true
- });
- console.log('MutationObserver set up');
- });
- })();
-
- //--Inline replies
-
- //Auto TOS Accept with Delay
- (function() {
- 'use strict';
-
- // Check if on the disclaimer page
- if (window.location.pathname === '/.static/pages/disclaimer.html') {
- // Redirect to confirmed page after 1-second delay
- setTimeout(() => {
- window.location.replace('.static/pages/confirmed.html');
- console.log('Automatically redirected from disclaimer to confirmed page after 1-second delay');
- }, 1000);
- }
- })();
- //--Auto TOS Accept with Delay
-
- //Media Auto-Preview
- // Auto-preview images and videos on hover for un-expanded thumbnails, disabling native hover
- (function() {
- 'use strict';
-
- // Disable native hover preview
- localStorage.setItem('hoveringImage', 'false'); // Disable "Image Preview on Hover" setting
- if (window.thumbs && typeof window.thumbs.removeHoveringExpand === 'function') {
- window.thumbs.removeHoveringExpand(); // Remove native hover listeners
- }
- // Override addHoveringExpand to prevent re-enabling
- if (window.thumbs) {
- window.thumbs.addHoveringExpand = function() {
- // Do nothing to prevent native hover preview
- console.log('Native hover preview (addHoveringExpand) blocked by userscript');
- };
- }
-
- // Supported file extensions for images and videos
- const supportedExtensions = {
- image: ['.gif', '.webp', '.png', '.jfif', '.pjpeg', '.jpeg', '.pjp', '.jpg', '.bmp', '.dib', '.svgz', '.svg'],
- video: ['.webm', '.m4v', '.mp4', '.ogm', '.ogv', '.avi', '.asx', '.mpg', '.mpeg']
- };
-
- // Create preview container
- const previewContainer = document.createElement('div');
- previewContainer.style.position = 'fixed';
- previewContainer.style.zIndex = '1000';
- previewContainer.style.pointerEvents = 'none'; // Allow clicks to pass through
- previewContainer.style.display = 'none';
- document.body.appendChild(previewContainer);
-
- // Function to check if URL is a supported image or video
- function isSupportedMedia(url) {
- const ext = (url.match(/\.[a-z0-9]+$/i) || [''])[0].toLowerCase();
- return supportedExtensions.image.includes(ext) || supportedExtensions.video.includes(ext);
- }
-
- // Function to check if URL is a video
- function isVideo(url) {
- const ext = (url.match(/\.[a-z0-9]+$/i) || [''])[0].toLowerCase();
- return supportedExtensions.video.includes(ext);
- }
-
- // Function to check if link is in un-expanded state
- function isUnexpanded(link) {
- const thumbnail = link.querySelector('img:not(.imgExpanded)');
- const expanded = link.querySelector('img.imgExpanded');
- return thumbnail && window.getComputedStyle(thumbnail).display !== 'none' &&
- (!expanded || window.getComputedStyle(expanded).display === 'none');
- }
-
- // Function to calculate preview dimensions
- function getPreviewDimensions(naturalWidth, naturalHeight) {
- // Detect zoom level
- const zoomLevel = window.devicePixelRatio || 1; // Fallback to 1 if undefined
- // Content area (excludes scrollbar) for max size
- const maxWidth = document.documentElement.clientWidth;
- const maxHeight = document.documentElement.clientHeight;
- // Screen resolution for small media check
- const screenWidth = window.screen.width || 1920; // Fallback to 1920
- const screenHeight = window.screen.height || 1080; // Fallback to 1080
-
- // If media fits within screen resolution, use full native size
- if (naturalWidth <= screenWidth && naturalHeight <= screenHeight) {
- let width = naturalWidth;
- let height = naturalHeight;
-
- // If native size exceeds content area, scale down
- const scaleByWidth = maxWidth / width;
- const scaleByHeight = maxHeight / height;
- const scale = Math.min(scaleByWidth, scaleByHeight, 1);
- width = Math.round(width * scale);
- height = Math.round(height * scale);
-
- return { width, height };
- }
-
- // Otherwise, adjust for zoom and scale to fit content area
- let width = naturalWidth / zoomLevel;
- let height = naturalHeight / zoomLevel;
-
- const scaleByWidth = maxWidth / width;
- const scaleByHeight = maxHeight / height;
- const scale = Math.min(scaleByWidth, scaleByHeight, 1);
- width = Math.round(width * scale);
- height = Math.round(height * scale);
-
- return { width, height };
- }
-
- // Function to position preview near cursor
- function positionPreview(event) {
- const mouseX = event.clientX;
- const mouseY = event.clientY;
- const previewWidth = previewContainer.offsetWidth;
- const previewHeight = previewContainer.offsetHeight;
-
- // Skip if dimensions are not yet available
- if (previewWidth === 0 || previewHeight === 0) {
- return;
- }
-
- // Use content area for positioning (excludes scrollbar)
- const maxWidth = document.documentElement.clientWidth;
- const maxHeight = document.documentElement.clientHeight;
-
- // Calculate centered position
- const centerX = (maxWidth - previewWidth) / 2;
- const centerY = (maxHeight - previewHeight) / 2;
-
- // Allow cursor to influence position with a bounded offset
- const maxOffset = 100; // Maximum pixels to shift from center
- const cursorOffsetX = Math.max(-maxOffset, Math.min(maxOffset, mouseX - maxWidth / 2));
- const cursorOffsetY = Math.max(-maxOffset, Math.min(maxOffset, mouseY - maxHeight / 2));
-
- // Calculate initial position with cursor influence
- let left = centerX + cursorOffsetX;
- let top = centerY + cursorOffsetY;
-
- // Ensure preview stays fully within content area
- left = Math.max(0, Math.min(left, maxWidth - previewWidth));
- top = Math.max(0, Math.min(top, maxHeight - previewHeight));
-
- previewContainer.style.left = `${left}px`;
- previewContainer.style.top = `${top}px`;
- }
-
- // Function to show preview
- function showPreview(link, event) {
- if (!isUnexpanded(link)) return; // Skip if expanded
- const url = link.href;
- if (!isSupportedMedia(url)) return;
-
- // Clear existing preview
- previewContainer.innerHTML = '';
-
- if (isVideo(url)) {
- // Create video element
- const video = document.createElement('video');
- video.src = url;
- video.autoplay = true;
- video.muted = false; // Play with audio
- video.loop = true;
- video.style.maxWidth = '100%';
- video.style.maxHeight = '100%';
-
- // Set dimensions and position when metadata is loaded
- video.onloadedmetadata = () => {
- const { width, height } = getPreviewDimensions(video.videoWidth, video.videoHeight);
- video.width = width;
- video.height = height;
- previewContainer.style.width = `${width}px`;
- previewContainer.style.height = `${height}px`;
- previewContainer.style.display = 'block'; // Show after dimensions are set
- positionPreview(event);
- };
-
- previewContainer.appendChild(video);
- } else {
- // Create image element
- const img = document.createElement('img');
- img.src = url;
- img.style.maxWidth = '100%';
- img.style.maxHeight = '100%';
-
- // Set dimensions and position when image is loaded
- img.onload = () => {
- const { width, height } = getPreviewDimensions(img.naturalWidth, img.naturalHeight);
- img.width = width;
- img.height = height;
- previewContainer.style.width = `${width}px`;
- previewContainer.style.height = `${height}px`;
- previewContainer.style.display = 'block'; // Show after dimensions are set
- positionPreview(event);
- };
-
- previewContainer.appendChild(img);
- }
- }
-
- // Function to hide preview
- function hidePreview() {
- previewContainer.style.display = 'none';
- // Stop video playback
- const video = previewContainer.querySelector('video');
- if (video) {
- video.pause();
- video.currentTime = 0;
- }
- previewContainer.innerHTML = '';
- }
-
- // Function to apply hover events to links
- function applyHoverEvents(container = document) {
- const links = container.querySelectorAll('.uploadCell a.imgLink');
- links.forEach(link => {
- // Skip if already processed
- if (link.dataset.previewProcessed) return;
- link.dataset.previewProcessed = 'true';
-
- link.addEventListener('mouseenter', (e) => {
- showPreview(link, e);
- });
-
- link.addEventListener('mousemove', (e) => {
- if (previewContainer.style.display === 'block') {
- positionPreview(e);
- }
- });
-
- link.addEventListener('mouseleave', () => {
- hidePreview();
- });
-
- // Hide preview on click if expanded
- link.addEventListener('click', () => {
- if (!isUnexpanded(link)) {
- hidePreview();
- }
- });
- });
- }
-
- // Apply hover events to existing links on page load
- applyHoverEvents();
- console.log('Media preview events applied on page load');
-
- // Patch inline reply logic to apply hover events to new inline content
- if (window.tooltips) {
- // Patch loadTooltip to apply hover events after content is loaded
- const originalLoadTooltip = tooltips.loadTooltip;
- tooltips.loadTooltip = function(element, quoteUrl, sourceId, isInline) {
- originalLoadTooltip.apply(this, arguments);
- if (isInline) {
- setTimeout(() => {
- applyHoverEvents(element);
- console.log('Media preview events applied to loaded tooltip content:', quoteUrl);
- }, 0);
- }
- };
-
- // Patch addLoadedTooltip to ensure hover events are applied
- const originalAddLoadedTooltip = tooltips.addLoadedTooltip;
- tooltips.addLoadedTooltip = function(htmlContents, tooltip, quoteUrl, replyId, isInline) {
- originalAddLoadedTooltip.apply(this, arguments);
- if (isInline) {
- applyHoverEvents(htmlContents);
- console.log('Media preview events applied to inline tooltip content:', quoteUrl);
- }
- };
- }
-
- // Set up MutationObserver to handle dynamically added posts
- const observer = new MutationObserver(mutations => {
- mutations.forEach(mutation => {
- if (mutation.addedNodes.length) {
- mutation.addedNodes.forEach(node => {
- if (node.nodeType === Node.ELEMENT_NODE) {
- // Handle new posts and inline replies
- const newLinks = node.matches('.uploadCell a.imgLink') ? [node] : node.querySelectorAll('.uploadCell a.imgLink');
- newLinks.forEach(link => {
- applyHoverEvents(link.parentElement);
- console.log('Media preview events applied to new link:', link.href);
- });
- }
- });
- }
- });
- });
-
- // Observe changes to the posts container
- const postsContainer = document.querySelector('.divPosts') || document.body;
- observer.observe(postsContainer, {
- childList: true,
- subtree: true
- });
- })();
- //--Media Auto-Preview
-
- //Post Age Tooltip
- // Show a tooltip with time elapsed since post when hovering over date/time
- (function() {
- 'use strict';
-
- // Create tooltip container
- const tooltip = document.createElement('div');
- tooltip.style.position = 'fixed';
- tooltip.style.zIndex = '1000';
- tooltip.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
- tooltip.style.color = '#fff';
- tooltip.style.padding = '5px 10px';
- tooltip.style.borderRadius = '4px';
- tooltip.style.fontSize = '12px';
- tooltip.style.pointerEvents = 'none';
- tooltip.style.display = 'none';
- document.body.appendChild(tooltip);
-
- // Parse timestamp (e.g., "04/16/2025 (Wed) 21:23:21")
- function parseTimestamp(text) {
- const match = text.match(/^(\d{2})\/(\d{2})\/(\d{4}).*?(\d{2}):(\d{2}):(\d{2})$/);
- if (!match) return null;
- const [, month, day, year, hours, minutes, seconds] = match;
- const isoString = `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`;
- const date = new Date(isoString);
- return isNaN(date.getTime()) ? null : date;
- }
-
- // Format elapsed time
- function formatElapsedTime(postDate) {
- const now = new Date();
- const diffMs = now - postDate;
- if (diffMs < 0) return 'Just now';
-
- const diffSeconds = Math.floor(diffMs / 1000);
- if (diffSeconds < 60) {
- return `${diffSeconds} second${diffSeconds === 1 ? '' : 's'} ago`;
- }
-
- const diffMinutes = Math.floor(diffSeconds / 60);
- if (diffMinutes < 60) {
- return `${diffMinutes} minute${diffMinutes === 1 ? '' : 's'} ago`;
- }
-
- const diffHours = Math.floor(diffMinutes / 60);
- if (diffHours < 24) {
- const remainingMinutes = diffMinutes % 60;
- return `${diffHours} hour${diffHours === 1 ? '' : 's'} and ${remainingMinutes} minute${remainingMinutes === 1 ? '' : 's'} ago`;
- }
-
- const diffDays = Math.floor(diffHours / 24);
- const remainingHours = diffHours % 24;
- return `${diffDays} day${diffDays === 1 ? '' : 's'} and ${remainingHours} hour${remainingHours === 1 ? '' : 's'} ago`;
- }
-
- // Position tooltip above element
- function positionTooltip(event, element) {
- const rect = element.getBoundingClientRect();
- const left = event.clientX;
- const top = rect.top - tooltip.offsetHeight - 5;
-
- tooltip.style.left = `${left}px`;
- tooltip.style.top = `${top}px`;
- }
-
- // Show tooltip
- function showTooltip(element, event) {
- const postDate = parseTimestamp(element.textContent);
- if (!postDate) {
- tooltip.style.display = 'none';
- return;
- }
-
- tooltip.textContent = formatElapsedTime(postDate);
- tooltip.style.display = 'block';
- positionTooltip(event, element);
- }
-
- // Hide tooltip
- function hideTooltip() {
- tooltip.style.display = 'none';
- }
-
- // Apply tooltip events to labelCreated elements
- function applyTooltipEvents(container = document) {
- const dateSpans = container.querySelectorAll('span.labelCreated');
- dateSpans.forEach(span => {
- // Remove existing listeners to avoid duplicates
- span.removeEventListener('mouseenter', showTooltip);
- span.removeEventListener('mouseleave', hideTooltip);
-
- span.addEventListener('mouseenter', (e) => {
- showTooltip(span, e);
- });
-
- span.addEventListener('mouseleave', () => {
- hideTooltip();
- });
- });
- }
-
- // Apply tooltip events on page load
- applyTooltipEvents();
- console.log('Post age tooltip events applied on page load');
-
- // Patch inline reply logic
- if (window.tooltips) {
- const originalLoadTooltip = tooltips.loadTooltip;
- tooltips.loadTooltip = function(element, quoteUrl, sourceId, isInline) {
- originalLoadTooltip.apply(this, arguments);
- if (isInline) {
- setTimeout(() => {
- applyTooltipEvents(element);
- console.log('Post age tooltip events applied to loaded tooltip content:', quoteUrl);
- }, 0);
- }
- };
-
- const originalAddLoadedTooltip = tooltips.addLoadedTooltip;
- tooltips.addLoadedTooltip = function(htmlContents, tooltip, quoteUrl, replyId, isInline) {
- originalAddLoadedTooltip.apply(this, arguments);
- if (isInline) {
- applyTooltipEvents(htmlContents);
- console.log('Post age tooltip events applied to inline tooltip content:', quoteUrl);
- }
- };
- }
-
- //Force-Enable Local Times
- (function() {
- 'use strict';
- localStorage.setItem('localTime', 'true');
- console.log('Local Times setting enabled');
- })();
-
- // MutationObserver for dynamically added posts
- const observer = new MutationObserver(mutations => {
- mutations.forEach(mutation => {
- if (mutation.addedNodes.length) {
- mutation.addedNodes.forEach(node => {
- if (node.nodeType === Node.ELEMENT_NODE) {
- const newSpans = node.matches('span.labelCreated') ? [node] : node.querySelectorAll('span.labelCreated');
- newSpans.forEach(span => {
- applyTooltipEvents(span.parentElement);
- console.log('Post age tooltip events applied to new span:', span.textContent);
- });
- }
- });
- }
- });
- });
-
- // Observe posts container
- const postsContainer = document.querySelector('.divPosts') || document.body;
- observer.observe(postsContainer, {
- childList: true,
- subtree: true
- });
- })();
- //--Post Age Tooltip
-
- //Last Read Post Tracker
- (function() {
- 'use strict';
-
- // Only run on thread pages (e.g., /vyt/res/24600.html)
- if (!window.location.pathname.match(/\/res\/\d+\.html$/)) {
- console.log('Not a thread page, exiting Last Read Post Tracker');
- return;
- }
-
- // Get thread ID from URL (e.g., "24600" from /vyt/res/24600.html)
- const threadIdMatch = window.location.pathname.match(/(\d+)\.html$/);
- if (!threadIdMatch) {
- console.error('Could not extract thread ID from URL:', window.location.pathname);
- return;
- }
- const threadId = threadIdMatch[1];
-
- // Load last read posts from localStorage
- let lastReadPosts = {};
- try {
- lastReadPosts = JSON.parse(localStorage.getItem('lastReadPosts') || '{}');
- } catch (e) {
- console.error('Failed to parse lastReadPosts from localStorage:', e);
- }
- let lastReadPostId = lastReadPosts[threadId] || null;
- let currentArrow = null;
-
- // Throttle function to limit scroll event frequency
- function throttle(fn, wait) {
- let lastCall = 0;
- return function(...args) {
- const now = Date.now();
- if (now - lastCall >= wait) {
- lastCall = now;
- fn(...args);
- }
- };
- }
-
- // Add arrow to a post (only called on page load)
- function addArrow(postContainer) {
- if (currentArrow) {
- currentArrow.remove();
- currentArrow = null;
- }
- const postInfo = postContainer.querySelector('.postInfo.title');
- if (!postInfo) {
- console.error('postInfo.title not found in postContainer:', postContainer.outerHTML);
- return;
- }
- const arrow = document.createElement('span');
- arrow.textContent = '→';
- arrow.style.color = '#ff0000';
- arrow.style.marginLeft = '5px';
- postInfo.appendChild(arrow);
- currentArrow = arrow;
- console.log(`Added arrow to post on load: ${postContainer.id || postContainer.className}`);
- }
-
- // Update last read post based on scroll position (no arrow during scroll)
- function updateLastReadPost() {
- const posts = document.querySelectorAll('.postCell, .postContainer');
- if (!posts.length) {
- console.warn('No post elements found. Available classes:',
- Array.from(document.querySelectorAll('[class*="post"], [class*="reply"]'))
- .map(el => el.className)
- .filter((v, i, a) => a.indexOf(v) === i));
- // Retry after a short delay if no posts are found
- setTimeout(() => requestAnimationFrame(updateLastReadPost), 500);
- return;
- }
-
- let newLastReadPostId = lastReadPostId;
- posts.forEach(post => {
- const rect = post.getBoundingClientRect();
- // Extract post ID from id or linkQuote
- let postId = post.id.match(/^(?:pc|p|post-)?(\d+)$/)?.[1];
- if (!postId) {
- const linkQuote = post.querySelector('.linkQuote');
- postId = linkQuote?.textContent.trim().replace('>>', '') || null;
- }
- if (!postId) {
- console.warn('Could not extract post ID from:', post.outerHTML);
- return;
- }
- // Consider post read if its top is above viewport center and visible
- if (rect.top < window.innerHeight / 2 && rect.bottom > 0) {
- newLastReadPostId = postId;
- }
- });
-
- if (newLastReadPostId && newLastReadPostId !== lastReadPostId) {
- lastReadPostId = newLastReadPostId;
- console.log(`Tracked last read post for thread ${threadId}: ${lastReadPostId} (no arrow)`);
- }
- }
-
- // Save last read post to localStorage when leaving the thread
- function saveLastReadPost() {
- if (lastReadPostId) {
- lastReadPosts[threadId] = lastReadPostId;
- try {
- localStorage.setItem('lastReadPosts', JSON.stringify(lastReadPosts));
- console.log(`Saved last read post for thread ${threadId}: ${lastReadPostId}`);
- } catch (e) {
- console.error('Failed to save lastReadPosts to localStorage:', e);
- }
- }
- }
-
- // Scroll to last read post on load and show arrow (MODIFIED for v2.5.2 base)
- function scrollToLastReadPost() {
- // --- Check for conditions where we should NOT scroll to the stored last read post ---
- const currentHash = window.location.hash;
- const referrer = document.referrer; // Check referrer
- // Check if referrer is from an overboard page
- const isFromOverboard = referrer.includes('/overboard') || referrer.includes('/sfw');
- // Check if the hash targets a specific post (e.g., #12345)
- const hasPostHash = currentHash.match(/^#(\d+)$/);
-
- // Condition 1: Came from overboard AND there's a specific post hash (#postId)
- if (hasPostHash && isFromOverboard) {
- console.log('[Last Read Tracker] Overboard navigation with post hash detected. Skipping scroll to last read post. Allowing default browser scroll.');
- // Let the browser handle the scroll based on the hash.
- // The Shared Link Handler below will also see this hash but might refine scroll/clear hash later if needed.
- return; // Exit without scrolling to stored position
- }
-
- // Condition 2: Page loaded with a specific post hash (#postId), even if not from overboard
- // This check prevents this module from overriding the Shared Link Handler's job,
- // which should handle scrolling to the *target* post ID in this case.
- if (hasPostHash && !isFromOverboard) {
- console.log('[Last Read Tracker] Initial post hash detected (not from overboard). Skipping scroll to stored last read post (handled by Shared Link Handler).');
- return; // Exit without scrolling to stored position
- }
-
- // --- If neither condition above was met, proceed with original logic ---
- // Check if we have a stored lastReadPostId for this thread
- if (lastReadPostId) {
- // Find the post container using the lastReadPostId FROM STORAGE
- // Original querySelector from v2.5.2:
- const postContainer = document.querySelector(`[id="pc${lastReadPostId}"], [id="p${lastReadPostId}"], [id="post-${lastReadPostId}"], .postCell .linkQuote[href*="${lastReadPostId}"], .postContainer .linkQuote[href*="${lastReadPostId}"]`)?.closest('.postCell, .postContainer');
-
- if (postContainer) {
- // Scroll to the *stored* last read post because no initial #postId hash was present
- postContainer.scrollIntoView({ behavior: 'smooth', block: 'center' });
- addArrow(postContainer); // Add arrow only when scrolling to stored position
- console.log(`[Last Read Tracker] Scrolled to stored last read post: ${lastReadPostId}`);
- } else {
- // Post container not found in DOM yet, maybe retry
- console.warn(`[Last Read Tracker] Stored last read post container for ID ${lastReadPostId} not found, retrying in 500ms...`);
- // Retry only if the DOM might still be loading (don't retry indefinitely if Conditions 1 or 2 were met earlier)
- setTimeout(scrollToLastReadPost, 500);
- }
- } else {
- // No stored last read post ID for this thread
- console.log('[Last Read Tracker] No stored last read post found for thread:', threadId);
- }
- }
-
- // Wait for DOM to be fully ready
- function initialize() {
- if (document.readyState === 'complete' || document.readyState === 'interactive') {
- scrollToLastReadPost();
- // Attach throttled scroll handler using requestAnimationFrame
- const throttledUpdate = throttle(() => requestAnimationFrame(updateLastReadPost), 200);
- window.addEventListener('scroll', throttledUpdate);
- // Log DOM state for debugging
- console.log('Initial post elements found:', document.querySelectorAll('.postCell, .postContainer').length);
- } else {
- setTimeout(initialize, 100); // Retry until DOM is ready
- }
- }
-
- // Start initialization
- initialize();
-
- // Save last read post when leaving the thread
- window.addEventListener('beforeunload', saveLastReadPost);
-
- console.log('Last Read Post Tracker initialized for thread', threadId);
- })();
- //--Last Read Post Tracker
-
- //Post Number Click Hash Purge
- (function() {
- 'use strict';
-
- // Event delegation for post number clicks (.linkQuote with #q<postId> in .postInfo.title)
- document.addEventListener('click', function(e) {
- const link = e.target.closest('.postInfo.title .linkQuote[href*="#q"]');
- if (link) {
- e.preventDefault(); // Block qr.js's default hash-setting
- e.stopPropagation(); // Stop other handlers
- const postId = link.href.match(/#q(\d+)/)?.[1];
- if (!postId) {
- console.warn('Could not extract post ID from link:', link.href);
- return;
- }
- const post = document.getElementById(postId);
- if (!post) {
- console.warn(`Post ${postId} not found for quick reply`);
- return;
- }
-
- // Preserve current scroll position
- const scrollX = window.scrollX;
- const scrollY = window.scrollY;
- console.log(`Preserving scroll position: x=${scrollX}, y=${scrollY}`);
-
- // Temporarily block scrollIntoView to prevent qr.js scrolling
- const originalScrollIntoView = Element.prototype.scrollIntoView;
- Element.prototype.scrollIntoView = function() {
- console.log(`Blocked scrollIntoView for post ${postId} during click`);
- };
-
- // Manually trigger quick reply
- if (window.qr && typeof window.qr.showQr === 'function') {
- window.qr.showQr(postId);
- // Restore scrollIntoView
- Element.prototype.scrollIntoView = originalScrollIntoView;
- // Clear any hash using only history.replaceState
- history.replaceState(null, '', window.location.pathname);
- // Ensure no residual hash
- if (window.location.hash) {
- console.log(`Residual hash detected: ${window.location.hash}, clearing`);
- history.replaceState(null, '', window.location.pathname);
- }
- // Restore scroll position to counter any changes
- window.scrollTo(scrollX, scrollY);
- console.log(`Post number click #q${postId}, opened quick reply, cleared hash, restored scroll: x=${scrollX}, y=${scrollY}`);
- } else {
- console.warn('qr.showQr not available, falling back to default behavior');
- // Allow default behavior if qr.js is unavailable
- Element.prototype.scrollIntoView = originalScrollIntoView;
- window.location.hash = `#q${postId}`;
- }
- }
- }, true);
- })();
- //--Post Number Click Hash Purge
-
- //Quick Reply Clear Button
- (function() {
- 'use strict';
-
- // Function to add Clear button to quick reply form
- function addClearButton() {
- const qrForm = document.querySelector('#quick-reply');
- if (!qrForm) {
- console.log('Quick reply form not found');
- return;
- }
-
- // Check if Clear button already exists
- if (qrForm.querySelector('.qr-clear-button')) {
- console.log('Clear button already added');
- return;
- }
-
- // Create Clear button
- const clearButton = document.createElement('button');
- clearButton.type = 'button'; // Prevent form submission
- clearButton.className = 'qr-clear-button';
- clearButton.textContent = 'Clear';
- clearButton.style.marginLeft = '5px';
- clearButton.style.padding = '2px 6px';
- clearButton.style.cursor = 'pointer';
- clearButton.style.border = '1px solid';
- clearButton.style.borderRadius = '3px';
-
- // Add click handler to clear all fields
- clearButton.addEventListener('click', () => {
- const qrBody = qrForm.querySelector('#qrbody');
- const qrName = qrForm.querySelector('#qrname');
- const qrSubject = qrForm.querySelector('#qrsubject');
- if (qrBody) qrBody.value = '';
- if (qrName) qrName.value = '';
- if (qrSubject) qrSubject.value = '';
- console.log('Cleared all quick reply fields');
- });
-
- // Insert button after the submit button or at the end of the form
- const submitButton = qrForm.querySelector('input[type="submit"]');
- if (submitButton) {
- submitButton.insertAdjacentElement('afterend', clearButton);
- } else {
- qrForm.appendChild(clearButton);
- }
- console.log('Added Clear button to quick reply form');
- }
-
- // Function to clear message body only
- function clearMessageBody() {
- const qrBody = document.querySelector('#qrbody');
- if (qrBody) {
- qrBody.value = '';
- console.log('Cleared quick reply message body');
- } else {
- console.log('Quick reply message body not found');
- }
- }
-
- // Track quick reply display state
- let isQrVisible = document.querySelector('#quick-reply') && window.getComputedStyle(document.querySelector('#quick-reply')).display !== 'none';
-
- // Observe quick reply form for display changes
- const observer = new MutationObserver(mutations => {
- mutations.forEach(mutation => {
- if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
- const qrForm = document.querySelector('#quick-reply');
- if (!qrForm) return;
- const isNowVisible = window.getComputedStyle(qrForm).display !== 'none';
- if (isNowVisible && !isQrVisible) {
- // Quick reply opened
- addClearButton();
- console.log('Quick reply opened, added Clear button');
- } else if (!isNowVisible && isQrVisible) {
- // Quick reply closed
- clearMessageBody();
- }
- isQrVisible = isNowVisible;
- }
- });
- });
-
- // Start observing the quick reply form (if it exists)
- const qrForm = document.querySelector('#quick-reply');
- if (qrForm) {
- observer.observe(qrForm, {
- attributes: true,
- attributeFilter: ['style']
- });
- // Initial check
- if (window.getComputedStyle(qrForm).display !== 'none') {
- addClearButton();
- isQrVisible = true;
- }
- }
-
- // Handle direct close button clicks
- document.addEventListener('click', (e) => {
- if (e.target.closest('.close-btn')) {
- clearMessageBody();
- }
- }, true);
-
- console.log('Quick Reply Clear Button initialized');
- })();
- //--Quick Reply Clear Button
-
- //Hash Quote Click Hash Purge
- (function() {
- 'use strict';
-
- // Event delegation for hash quote clicks (.hash-link)
- document.addEventListener('click', function(e) {
- if (e.target.classList.contains('hash-link')) {
- e.preventDefault(); // Block original Hash navigation handler
- e.stopPropagation(); // Stop other handlers
- const link = e.target.closest('.hash-link-container').previousElementSibling;
- if (!link || !link.textContent.startsWith('>>')) {
- console.warn('Invalid hash link or no associated quote:', e.target);
- return;
- }
- const postId = link.textContent.replace('>>', '');
- const post = document.getElementById(postId);
- if (!post) {
- console.warn(`Post ${postId} not found for hash quote navigation`);
- return;
- }
-
- // Preserve current scroll position as fallback
- const scrollX = window.scrollX;
- const scrollY = window.scrollY;
- console.log(`Preserving scroll position for hash quote: x=${scrollX}, y=${scrollY}`);
-
- // Scroll to post
- post.scrollIntoView({ behavior: 'smooth', block: 'center' });
- // Set hash temporarily to trigger scroll (if needed by browser)
- window.location.hash = `#${postId}`;
- // Immediately clear hash
- history.replaceState(null, '', window.location.pathname);
- // Ensure no residual hash
- if (window.location.hash) {
- console.log(`Residual hash detected: ${window.location.hash}, clearing`);
- history.replaceState(null, '', window.location.pathname);
- }
- // Restore scroll position if browser overrides
- window.scrollTo(scrollX, scrollY);
- console.log(`Hash quote click #${postId}, scrolled to post, cleared hash, restored scroll: x=${scrollX}, y=${scrollY}`);
- }
- }, true);
- })();
- //--Hash Quote Click Hash Purge
-
- //Shared Post Link Handler with Overboard Handling (for v2.5.2 base)
- (function() {
- 'use strict';
-
- // Only run on thread pages
- if (!window.location.pathname.match(/\/res\/\d+\.html$/)) {
- // console.log('[Shared Link Handler] Not a thread page, exiting.');
- return;
- }
-
- const initialHash = window.location.hash;
- const referrer = document.referrer;
- const isFromOverboard = referrer.includes('/overboard') || referrer.includes('/sfw');
- console.log(`[Shared Link Handler] Initial Load - Hash: "${initialHash}", Referrer: "${referrer}", FromOverboard: ${isFromOverboard}`);
-
- const postIdMatch = initialHash.match(/^#(\d+)$/);
- const isDirectPostLink = !!postIdMatch;
- const targetPostId = postIdMatch ? postIdMatch[1] : null;
-
- // Handle direct shared links (e.g., #123456)
- if (isDirectPostLink && targetPostId) {
- console.log(`[Shared Link Handler] Direct post link detected: #${targetPostId}`);
- // The modified Last Read Tracker already prevents scrolling to the *stored* position
- // if this hash exists. Now we just need to handle the scrolling *to the target*
- // and the hash clearing, respecting the overboard case.
-
- window.addEventListener('load', () => {
- // Use a small timeout to allow the browser's potential initial scroll to happen first
- setTimeout(() => {
- const post = document.getElementById(targetPostId) ||
- document.querySelector(`.postCell .linkQuote[href*="${targetPostId}"], .postContainer .linkQuote[href*="${targetPostId}"]`)?.closest('.postCell, .postContainer');
-
- if (post) {
- if (!isFromOverboard) {
- // If NOT from overboard, ensure we scroll smoothly to the target post
- console.log('[Shared Link Handler] Scrolling to target post (not from overboard).');
- post.scrollIntoView({ behavior: 'smooth', block: 'center' });
- // Clear the hash AFTER scrolling to prevent conflicts
- history.replaceState(null, '', window.location.pathname);
- console.log('[Shared Link Handler] Cleared shared post hash after scrolling.');
- } else {
- // If FROM overboard, the browser should handle the initial scroll.
- // We *still* want to clear the hash afterwards to prevent conflicts
- // with the Last Read Tracker saving logic or subsequent interactions.
- console.log('[Shared Link Handler] From overboard link. Browser should have scrolled. Clearing hash.');
- history.replaceState(null, '', window.location.pathname);
- console.log('[Shared Link Handler] Cleared shared post hash (from overboard).');
- }
- } else {
- // Post specified in hash not found
- console.warn(`[Shared Link Handler] Shared post ${targetPostId} not found.`);
- // Clear the invalid hash anyway
- history.replaceState(null, '', window.location.pathname);
- console.log('[Shared Link Handler] Cleared non-existent shared post hash.');
- }
- }, 100); // 100ms delay
- }, { once: true });
- }
- // Handle quick reply hashes (#q<postId>) on load (regardless of referrer)
- else if (initialHash.match(/^#q\d+$/)) {
- console.log('[Shared Link Handler] Quick reply hash detected on load.');
- window.addEventListener('load', () => {
- // Preserve current scroll position (could be 0,0 or where Last Read Tracker scrolled)
- const scrollX = window.scrollX;
- const scrollY = window.scrollY;
- // Clear quick reply hash
- history.replaceState(null, '', window.location.pathname);
- // Restore scroll position just in case clearing the hash triggered a scroll
- window.scrollTo(scrollX, scrollY);
- console.log(`[Shared Link Handler] Cleared quick reply hash ${initialHash} on load, restored scroll: x=${scrollX}, y=${scrollY}`);
- }, { once: true });
- }
- // If no initial hash, do nothing - Last Read Tracker handles it.
-
- // --- Hash Change Listener ---
- // Block ALL default hashchange scrolling triggered by site scripts or manual hash changes
- // for post/QR hashes, as the script should manage scrolling and state.
- window.addEventListener('hashchange', (e) => {
- const currentHash = window.location.hash;
- // Check for #q<digits> or #<digits>
- if (currentHash.match(/^#(q)?\d+$/)) {
- console.log(`[Shared Link Handler] Hashchange event detected for ${currentHash}. Preventing default and clearing.`);
- e.preventDefault(); // Prevent default scroll/action
- e.stopPropagation(); // Prevent other listeners (like site's qr.js)
- const scrollX = window.scrollX;
- const scrollY = window.scrollY;
- // Clear the hash immediately
- history.replaceState(null, '', window.location.pathname);
- // Restore scroll position
- window.scrollTo(scrollX, scrollY);
- // console.log(`[Shared Link Handler] Blocked hashchange, cleared hash ${currentHash}, restored scroll: x=${scrollX}, y=${scrollY}`);
- }
- }, true); // Use capture phase
-
- console.log('[Shared Link Handler] Initialized.');
- })();
- //--Shared Post Link Handler with Overboard Handling (for v2.5.2 base)