// ==UserScript== // @name Enhanced 8chan UI // @version 1.6.7 // @description Creates a media gallery with blur toggle and live thread info (Posts, Users, Files) plus additional enhancements // @match https://8chan.moe/*/res/* // @match https://8chan.se/*/res/* // @grant GM_addStyle // @grant GM.addStyle // @license MIT // @namespace https://greasyfork.org/users/1459581 // @downloadURL none // ==/UserScript== (function () { 'use strict'; // Check if we're on a thread page const isThreadPage = window.location.href.match(/https:\/\/8chan\.moe\/.*\/res\/.*/); // Default configuration for additional features var defaultConfig = {}; // TODO add menu and default configs to toggle options // Main gallery functionality let currentIndex = 0; const mediaElements = []; GM_addStyle(` .postCell { margin: 0 !important; } #navBoardsSpan { font-size: large; } .gallery-button { position: fixed; right: 20px; z-index: 9999; background: #333; color: white; padding: 15px; border-radius: 50%; cursor: pointer; box-shadow: 0 2px 5px rgba(0,0,0,0.3); text-align: center; line-height: 1; font-size: 20px; } .gallery-button.blur-toggle { bottom: 80px; } .gallery-button.gallery-open { bottom: 20px; } #media-count-display { position: fixed; bottom: 150px; right: 20px; background: #444; color: white; padding: 8px 12px; border-radius: 10px; font-size: 14px; z-index: 9999; box-shadow: 0 2px 5px rgba(0,0,0,0.3); white-space: nowrap; } .gallery-modal { display: none; position: fixed; bottom: 80px; right: 20px; width: 80%; max-width: 600px; max-height: 80vh; background: oklch(21% 0.006 285.885); border-radius: 10px; padding: 20px; overflow-y: auto; z-index: 9998; } .gallery-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); gap: 10px; } .media-item { position: relative; cursor: pointer; aspect-ratio: 1; overflow: hidden; border-radius: 5px; } .media-thumbnail { width: 100%; height: 100%; object-fit: cover; } .media-type-icon { position: absolute; bottom: 5px; right: 5px; color: white; background: rgba(0,0,0,0.5); padding: 2px 5px; border-radius: 3px; font-size: 0.8em; } .lightbox { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.9); z-index: 10000; } .lightbox-content { position: absolute; top: 45%; left: 50%; transform: translate(-50%, -50%); max-width: 90%; max-height: 90%; } .lightbox-video { max-width: 90vw; max-height: 90vh; } .close-btn { position: absolute; top: 20px; right: 20px; width: 50px; height: 50px; cursor: pointer; } .lightbox-nav { position: absolute; top: 50%; transform: translateY(-50%); background: rgba(255,255,255,0.2); color: white; border: none; padding: 15px; cursor: pointer; font-size: 24px; border-radius: 50%; } .lightbox-prev { left: 20px; } .lightbox-next { right: 20px; } .go-to-post-btn { position: absolute; bottom: 10px; left: 50%; transform: translateX(-50%); background: rgba(255,255,255,0.1); color: white; border: none; padding: 8px 15px; border-radius: 20px; cursor: pointer; font-size: 14px; } .blurred-media img, .blurred-media video, .blurred-media audio { filter: blur(10px) brightness(0.8); transition: filter 0.3s ease; } /* New styles for centered quick-reply */ #quick-reply.centered { position: fixed; top: 50% !important; left: 50% !important; transform: translate(-50%, -50%); width: 80%; max-width: 800px; min-height: 550px; background: oklch(21% 0.006 285.885); padding: 10px !important; border-radius: 10px; z-index: 9999; box-shadow: 0 0 20px rgba(0,0,0,0.5); } #quick-reply table, #quick-reply.centered #qrname, #quick-reply.centered #qrsubject, #quick-reply.centered #qrbody { width: 100% !important; max-width: 100% !important; box-sizing: border-box; } #quick-reply.centered #qrbody { min-height: 200px; } #quick-reply-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); z-index: 99; display: none; } /* Cleanup */ #footer, #postingForm, #actionsForm, #navTopBoardsSpan, .coloredIcon.linkOverboard, .coloredIcon.linkSfwOver, .coloredIcon.multiboardButton, #navLinkSpan>span:nth-child(9), #navLinkSpan>span:nth-child(11), #navLinkSpan>span:nth-child(13) { display: none; } /* Header */ #dynamicHeaderThread, .navHeader { box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15); } /* Thread Watcher */ #watchedMenu .floatingContainer { min-width: 330px; } #watchedMenu .watchedCellLabel > a:after { content: " - "attr(href); filter: saturate(50%); font-style: italic; font-weight: bold; } #watchedMenu { box-shadow: -3px 3px 2px 0px rgba(0,0,0,0.19); } /* Posts */ .quoteTooltip .innerPost { overflow: hidden; box-shadow: -3px 3px 2px 0px rgba(0,0,0,0.19); } /* Catalog page CSS */ #dynamicAnnouncement { display: none; } #postingForm { margin: 2em auto; } `); // Only create thread-specific UI elements if we're on a thread page if (isThreadPage) { // Create gallery UI elements const galleryButton = document.createElement('div'); galleryButton.className = 'gallery-button gallery-open'; galleryButton.textContent = '🎴'; galleryButton.title = 'Gallery'; document.body.appendChild(galleryButton); const blurToggle = document.createElement('div'); blurToggle.className = 'gallery-button blur-toggle'; blurToggle.textContent = '💼'; blurToggle.title = 'Goon Mode'; document.body.appendChild(blurToggle); const replyButton = document.createElement('div'); replyButton.id = 'replyButton'; replyButton.className = 'gallery-button'; replyButton.style.bottom = '190px'; replyButton.textContent = '✏️'; replyButton.title = 'Reply'; document.body.appendChild(replyButton); const mediaInfoDisplay = document.createElement('div'); mediaInfoDisplay.id = 'media-count-display'; document.body.appendChild(mediaInfoDisplay); // Create overlay for quick-reply const overlay = document.createElement('div'); overlay.id = 'quick-reply-overlay'; document.body.appendChild(overlay); let isBlurred = false; blurToggle.addEventListener('click', () => { isBlurred = !isBlurred; blurToggle.textContent = isBlurred ? '🍆' : '💼'; blurToggle.title = isBlurred ? 'SafeMode' : 'Goon Mode'; document.querySelectorAll('div.innerPost').forEach(post => { post.classList.toggle('blurred-media', isBlurred); }); }); function setupQuickReply() { const quickReply = document.getElementById('quick-reply'); if (!quickReply) return; // Create close button if it doesn't exist if (!quickReply.querySelector('.qr-close-btn')) { const closeBtn = document.createElement('div'); closeBtn.className = 'close-btn qr-close-btn'; closeBtn.textContent = ' '; closeBtn.style.position = 'absolute'; closeBtn.style.top = '10px'; closeBtn.style.right = '10px'; closeBtn.style.cursor = 'pointer'; closeBtn.addEventListener('click', () => { quickReply.classList.remove('centered'); overlay.style.display = 'none'; }); quickReply.appendChild(closeBtn); } quickReply.classList.add('centered'); overlay.style.display = 'block'; // Focus on reply body setTimeout(() => { document.querySelector('#qrbody')?.focus(); }, 100); } replyButton.addEventListener('click', () => { const nativeReplyBtn = document.querySelector('a#replyButton[href="#postingForm"]'); if (nativeReplyBtn) { nativeReplyBtn.click(); } else { location.hash = '#postingForm'; } // Clear form fields and setup centered quick-reply setTimeout(() => { document.querySelectorAll('#qrname, #qrsubject, #qrbody').forEach(field => { field.value = ''; }); setupQuickReply(); }, 100); }); const galleryModal = document.createElement('div'); galleryModal.className = 'gallery-modal'; const galleryGrid = document.createElement('div'); galleryGrid.className = 'gallery-grid'; galleryModal.appendChild(galleryGrid); document.body.appendChild(galleryModal); const lightbox = document.createElement('div'); lightbox.className = 'lightbox'; lightbox.innerHTML = `
×
`; document.body.appendChild(lightbox); function collectMedia() { mediaElements.length = 0; const seenUrls = new Set(); document.querySelectorAll('div.innerPost').forEach(post => { post.querySelectorAll('img[loading="lazy"]').forEach(img => { const src = img.src; if (!src || seenUrls.has(src)) return; const parentLink = img.closest('a'); const href = parentLink?.href; if (href && !seenUrls.has(href)) { seenUrls.add(href); mediaElements.push({ element: parentLink, thumbnail: img, url: href, type: /\.(mp4|webm|mov)$/i.test(href) ? 'VIDEO' : /\.(mp3|wav|ogg)$/i.test(href) ? 'AUDIO' : 'IMAGE', postElement: post }); } else { seenUrls.add(src); mediaElements.push({ element: img, thumbnail: img, url: src, type: 'IMAGE', postElement: post }); } }); post.querySelectorAll('a[href*=".media"]:not(:has(img)), a.imgLink:not(:has(img))').forEach(link => { const href = link.href; if (!href || seenUrls.has(href)) return; const ext = href.split('.').pop().toLowerCase(); if (/\.(jpg|jpeg|png|gif|webp|mp4|webm|mov|mp3|wav|ogg)$/i.test(ext)) { seenUrls.add(href); mediaElements.push({ element: link, thumbnail: null, url: href, type: /\.(mp4|webm|mov)$/i.test(ext) ? 'VIDEO' : /\.(mp3|wav|ogg)$/i.test(ext) ? 'AUDIO' : 'IMAGE', postElement: post }); } }); }); } function createGalleryItems() { galleryGrid.innerHTML = ''; mediaElements.forEach((media, index) => { const item = document.createElement('div'); item.className = 'media-item'; const thumbnail = document.createElement('img'); thumbnail.className = 'media-thumbnail'; thumbnail.loading = 'lazy'; thumbnail.src = media.thumbnail?.src || ( media.type === 'VIDEO' ? 'https://via.placeholder.com/100/333/fff?text=VID' : media.type === 'AUDIO' ? 'https://via.placeholder.com/100/333/fff?text=AUD' : media.url ); const typeIcon = document.createElement('div'); typeIcon.className = 'media-type-icon'; typeIcon.textContent = media.type === 'VIDEO' ? 'VID' : media.type === 'AUDIO' ? 'AUD' : 'IMG'; item.appendChild(thumbnail); item.appendChild(typeIcon); item.addEventListener('click', () => showLightbox(media, index)); galleryGrid.appendChild(item); }); } function showLightbox(media, index) { currentIndex = typeof index === 'number' ? index : mediaElements.indexOf(media); updateLightboxContent(); lightbox.style.display = 'block'; } function updateLightboxContent() { const media = mediaElements[currentIndex]; let content; if (media.type === 'AUDIO') { content = document.createElement('audio'); content.controls = true; content.className = 'lightbox-content'; content.src = media.url; } else if (media.type === 'VIDEO') { content = document.createElement('video'); content.controls = true; content.className = 'lightbox-content lightbox-video'; content.src = media.url; content.autoplay = true; content.loop = true; } else { content = document.createElement('img'); content.className = 'lightbox-content'; content.src = media.url; content.loading = 'eager'; } lightbox.querySelector('.lightbox-content')?.remove(); lightbox.querySelector('.go-to-post-btn')?.remove(); const goToPostBtn = document.createElement('button'); goToPostBtn.className = 'go-to-post-btn'; goToPostBtn.textContent = 'Go to post'; goToPostBtn.addEventListener('click', () => { lightbox.style.display = 'none'; media.postElement.scrollIntoView({ behavior: 'smooth', block: 'center' }); media.postElement.style.transition = 'box-shadow 0.5s ease'; media.postElement.style.boxShadow = '0 0 0 3px rgba(255, 255, 0, 0.5)'; setTimeout(() => { media.postElement.style.boxShadow = 'none'; }, 2000); }); lightbox.appendChild(content); lightbox.appendChild(goToPostBtn); } function navigate(direction) { currentIndex = (currentIndex + direction + mediaElements.length) % mediaElements.length; updateLightboxContent(); } function updateThreadInfoDisplay() { const postCount = document.getElementById('postCount')?.textContent || '0'; const userCount = document.getElementById('userCountLabel')?.textContent || '0'; const fileCount = document.getElementById('fileCount')?.textContent || '0'; mediaInfoDisplay.textContent = `Posts: ${postCount} | Users: ${userCount} | Files: ${fileCount}`; } lightbox.querySelector('.lightbox-prev').addEventListener('click', () => navigate(-1)); lightbox.querySelector('.lightbox-next').addEventListener('click', () => navigate(1)); lightbox.querySelector('.close-btn').addEventListener('click', () => { lightbox.style.display = 'none'; }); galleryButton.addEventListener('click', () => { collectMedia(); createGalleryItems(); galleryModal.style.display = galleryModal.style.display === 'block' ? 'none' : 'block'; }); document.addEventListener('click', (e) => { if (!galleryModal.contains(e.target) && !galleryButton.contains(e.target)) { galleryModal.style.display = 'none'; } }); document.addEventListener('keydown', (e) => { if (lightbox.style.display === 'block') { if (e.key === 'ArrowLeft') navigate(-1); if (e.key === 'ArrowRight') navigate(1); } if (e.key === 'Escape') { galleryModal.style.display = 'none'; lightbox.style.display = 'none'; const qrCloseBtn = document.querySelector('.quick-reply .close-btn, th .close-btn'); if (qrCloseBtn && typeof qrCloseBtn.click === 'function') { qrCloseBtn.click(); } const qrFields = document.querySelectorAll('#qrname, #qrsubject, #qrbody'); qrFields.forEach(field => { field.value = ''; }); // Also hide overlay and centered quick-reply document.getElementById('quick-reply-overlay').style.display = 'none'; document.getElementById('quick-reply')?.classList.remove('centered'); } if (e.altKey && e.key.toLowerCase() === 'z') { replyButton.click(); } }); // Initialize main gallery functionality collectMedia(); createGalleryItems(); updateThreadInfoDisplay(); setInterval(updateThreadInfoDisplay, 5000); } // The following features are available on all pages // Header Catalog Links // Function to append /catalog.html to links function appendCatalogToLinks() { const navboardsSpan = document.getElementById('navBoardsSpan'); if (navboardsSpan) { const links = navboardsSpan.getElementsByTagName('a'); for (let link of links) { if (link.href && !link.href.endsWith('/catalog.html')) { link.href += '/catalog.html'; } } } } // Initial call to append links on page load appendCatalogToLinks(); // Set up a MutationObserver to watch for changes in the #navboardsSpan div const observer = new MutationObserver(appendCatalogToLinks); const config = { childList: true, subtree: true }; const navboardsSpan = document.getElementById('navBoardsSpan'); if (navboardsSpan) { observer.observe(navboardsSpan, config); } // Scroll to last read post // Function to save the scroll position const MAX_PAGES = 50; // Maximum number of pages to store scroll positions const currentPage = window.location.href; // Specify pages to exclude from scroll position saving (supports wildcards) const excludedPagePatterns = [ /\/catalog\.html$/i, // Exclude any page ending with /catalog.html (case-insensitive) // Add more patterns as needed ]; // Function to check if current page matches any exclusion pattern function isExcludedPage(url) { return excludedPagePatterns.some(pattern => pattern.test(url)); } // Function to save the scroll position for the current page function saveScrollPosition() { // Check if the current page matches any excluded pattern if (isExcludedPage(currentPage)) { return; // Skip saving scroll position for excluded pages } const scrollPosition = window.scrollY; // Get the current vertical scroll position localStorage.setItem(`scrollPosition_${currentPage}`, scrollPosition); // Store it in localStorage with a unique key // Manage the number of stored scroll positions manageScrollStorage(); } // Function to restore the scroll position for the current page function restoreScrollPosition() { const savedPosition = localStorage.getItem(`scrollPosition_${currentPage}`); // Retrieve the saved position for the current page if (savedPosition) { window.scrollTo(0, parseInt(savedPosition, 10)); // Scroll to the saved position } } // Function to manage the number of stored scroll positions function manageScrollStorage() { const keys = Object.keys(localStorage).filter(key => key.startsWith('scrollPosition_')); // If the number of stored positions exceeds the limit, remove the oldest if (keys.length > MAX_PAGES) { // Sort keys by their creation time (assuming the order of keys reflects the order of storage) keys.sort((a, b) => { return localStorage.getItem(a) - localStorage.getItem(b); }); // Remove the oldest entries until we are within the limit while (keys.length > MAX_PAGES) { localStorage.removeItem(keys.shift()); } } } // Event listener to save scroll position before the page unloads window.addEventListener('beforeunload', saveScrollPosition); // Restore scroll position when the page loads window.addEventListener('load', restoreScrollPosition); // Fix for Image Hover (function () { 'use strict'; // Function to handle mouse movement function onMouseMove(event) { const img = document.querySelector('img[style*="position: fixed"]'); if (img) { // Get the viewport dimensions const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; // Calculate the new position let newX = event.clientX + 10; // Offset to avoid cursor overlap let newY = event.clientY + 10; // Offset to avoid cursor overlap // Ensure the image stays within the viewport if (newX + img.width > viewportWidth) { newX = viewportWidth - img.width - 10; // Adjust for right edge } if (newY + img.height > viewportHeight) { newY = viewportHeight - img.height - 10; // Adjust for bottom edge } // Update the image position img.style.left = `${newX}px`; img.style.top = `${newY}px`; } } // Function to handle mouse enter and leave function onMouseEnter() { document.addEventListener('mousemove', onMouseMove); } function onMouseLeave() { document.removeEventListener('mousemove', onMouseMove); } // Observe for the image to appear and disappear const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach((node) => { if (node.nodeType === Node.ELEMENT_NODE && node.matches('img[style*="position: fixed"]')) { onMouseEnter(); } }); mutation.removedNodes.forEach((node) => { if (node.nodeType === Node.ELEMENT_NODE && node.matches('img[style*="position: fixed"]')) { onMouseLeave(); } }); }); }); // Start observing the body for changes observer.observe(document.body, { childList: true, subtree: true }); })(); })();