// ==UserScript== // @name Enhanced 8chan UI // @version 1.7.0 // @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'; // CONFIG // ============================== const CONFIG = { keybinds: { toggleReply: "Alt+Z", // Open reply window closeModals: "Escape", // Close all modals/panels galleryPrev: "ArrowLeft", // Previous media in lightbox galleryNext: "ArrowRight", // Next media in lightbox quickReplyFocus: "Tab" // Focus quick-reply fields cycle }, scrollMemory: { maxPages: 50, excludedPatterns: [ /\/catalog\.html$/i // Exclude catalog pages ] } }; // STYLES // ============================== const STYLES = ` /* Post styling */ .postCell { margin: 0 !important; } /* Navigation and Header */ #navBoardsSpan { font-size: large; } #dynamicHeaderThread, .navHeader { box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15); } /* Gallery and control buttons */ .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 */ .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 */ .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; font-size: 24px; line-height: 50px; text-align: center; color: white; } .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; } /* Blur effect */ .blurred-media img, .blurred-media video, .blurred-media audio { filter: blur(10px) brightness(0.8); transition: filter 0.3s ease; } /* Quick reply styling */ #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.centered 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; } /* 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); } /* Quote tooltips */ .quoteTooltip .innerPost { overflow: hidden; box-shadow: -3px 3px 2px 0px rgba(0,0,0,0.19); } /* Hidden elements */ #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), #dynamicAnnouncement { display: none; } `; // UTILITY FUNCTIONS // ============================== const util = { isThreadPage() { return window.location.href.match(/https:\/\/8chan\.(moe|se)\/.*\/res\/.*/); }, createElement(tag, options = {}) { const element = document.createElement(tag); if (options.id) element.id = options.id; if (options.className) element.className = options.className; if (options.text) element.textContent = options.text; if (options.html) element.innerHTML = options.html; if (options.attributes) { Object.entries(options.attributes).forEach(([attr, value]) => { element.setAttribute(attr, value); }); } if (options.styles) { Object.entries(options.styles).forEach(([prop, value]) => { element.style[prop] = value; }); } if (options.events) { Object.entries(options.events).forEach(([event, handler]) => { element.addEventListener(event, handler); }); } if (options.parent) options.parent.appendChild(element); return element; } }; // GALLERY SYSTEM // ============================== const gallery = { mediaElements: [], currentIndex: 0, isBlurred: false, initialize() { this.createUIElements(); this.setupEventListeners(); this.collectMedia(); this.createGalleryItems(); this.updateThreadInfoDisplay(); setInterval(() => this.updateThreadInfoDisplay(), 5000); }, createUIElements() { // Gallery button this.galleryButton = util.createElement('div', { className: 'gallery-button gallery-open', text: '🎴', attributes: { title: 'Gallery' }, parent: document.body }); // Blur toggle this.blurToggle = util.createElement('div', { className: 'gallery-button blur-toggle', text: '💼', attributes: { title: 'Goon Mode' }, parent: document.body }); // Reply button this.replyButton = util.createElement('div', { id: 'replyButton', className: 'gallery-button', text: '✏️', attributes: { title: 'Reply' }, styles: { bottom: '190px' }, parent: document.body }); // Media info display this.mediaInfoDisplay = util.createElement('div', { id: 'media-count-display', parent: document.body }); // Quick reply overlay this.overlay = util.createElement('div', { id: 'quick-reply-overlay', parent: document.body }); // Gallery modal this.galleryModal = util.createElement('div', { className: 'gallery-modal', parent: document.body }); this.galleryGrid = util.createElement('div', { className: 'gallery-grid', parent: this.galleryModal }); // Lightbox this.lightbox = util.createElement('div', { className: 'lightbox', html: `
×
`, parent: document.body }); }, setupEventListeners() { // Blur toggle this.blurToggle.addEventListener('click', () => { this.isBlurred = !this.isBlurred; this.blurToggle.textContent = this.isBlurred ? '🍆' : '💼'; this.blurToggle.title = this.isBlurred ? 'SafeMode' : 'Goon Mode'; document.querySelectorAll('div.innerPost').forEach(post => { post.classList.toggle('blurred-media', this.isBlurred); }); }); // Reply button this.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 = ''; }); this.setupQuickReply(); }, 100); }); // Gallery button this.galleryButton.addEventListener('click', () => { this.collectMedia(); this.createGalleryItems(); this.galleryModal.style.display = this.galleryModal.style.display === 'block' ? 'none' : 'block'; }); // Lightbox navigation this.lightbox.querySelector('.lightbox-prev').addEventListener('click', () => this.navigate(-1)); this.lightbox.querySelector('.lightbox-next').addEventListener('click', () => this.navigate(1)); this.lightbox.querySelector('.close-btn').addEventListener('click', () => { this.lightbox.style.display = 'none'; }); // Close modals when clicking outside document.addEventListener('click', (e) => { if (!this.galleryModal.contains(e.target) && !this.galleryButton.contains(e.target)) { this.galleryModal.style.display = 'none'; } }); // Keyboard shortcuts document.addEventListener('keydown', (e) => this.handleKeyboardShortcuts(e)); }, handleKeyboardShortcuts(e) { const { keybinds } = CONFIG; // Close modals/panels if (e.key === keybinds.closeModals) { if (this.lightbox.style.display === 'block') { this.lightbox.style.display = 'none'; } this.galleryModal.style.display = 'none'; const qrCloseBtn = document.querySelector('.quick-reply .close-btn, th .close-btn'); if (qrCloseBtn && typeof qrCloseBtn.click === 'function') { qrCloseBtn.click(); } document.getElementById('quick-reply-overlay').style.display = 'none'; document.getElementById('quick-reply')?.classList.remove('centered'); } // Navigation in lightbox if (this.lightbox.style.display === 'block') { if (e.key === keybinds.galleryPrev) this.navigate(-1); if (e.key === keybinds.galleryNext) this.navigate(1); } // Toggle reply window const [mod, key] = keybinds.toggleReply.split('+'); if (e[`${mod.toLowerCase()}Key`] && e.key.toLowerCase() === key.toLowerCase()) { this.replyButton.click(); } // Quick-reply field cycling if (e.key === keybinds.quickReplyFocus) { const fields = ['#qrname', '#qrsubject', '#qrbody']; const active = document.activeElement; const currentIndex = fields.findIndex(sel => active.matches(sel)); if (currentIndex > -1) { e.preventDefault(); const nextIndex = (currentIndex + 1) % fields.length; document.querySelector(fields[nextIndex])?.focus(); } } }, setupQuickReply() { const quickReply = document.getElementById('quick-reply'); if (!quickReply) return; // Create close button if it doesn't exist if (!quickReply.querySelector('.qr-close-btn')) { util.createElement('div', { className: 'close-btn qr-close-btn', text: '×', styles: { position: 'absolute', top: '10px', right: '10px', cursor: 'pointer' }, events: { click: () => { quickReply.classList.remove('centered'); this.overlay.style.display = 'none'; } }, parent: quickReply }); } quickReply.classList.add('centered'); this.overlay.style.display = 'block'; // Focus on reply body setTimeout(() => { document.querySelector('#qrbody')?.focus(); }, 100); }, collectMedia() { this.mediaElements = []; const seenUrls = new Set(); document.querySelectorAll('div.innerPost').forEach(post => { // Get images 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); this.mediaElements.push({ element: parentLink, thumbnail: img, url: href, type: this.getMediaType(href), postElement: post }); } else { seenUrls.add(src); this.mediaElements.push({ element: img, thumbnail: img, url: src, type: 'IMAGE', postElement: post }); } }); // Get media links without images 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; if (this.isMediaFile(href)) { seenUrls.add(href); this.mediaElements.push({ element: link, thumbnail: null, url: href, type: this.getMediaType(href), postElement: post }); } }); }); }, getMediaType(url) { if (/\.(mp4|webm|mov)$/i.test(url)) return 'VIDEO'; if (/\.(mp3|wav|ogg)$/i.test(url)) return 'AUDIO'; return 'IMAGE'; }, isMediaFile(url) { return /\.(jpg|jpeg|png|gif|webp|mp4|webm|mov|mp3|wav|ogg)$/i.test(url); }, createGalleryItems() { this.galleryGrid.innerHTML = ''; this.mediaElements.forEach((media, index) => { const item = util.createElement('div', { className: 'media-item', parent: this.galleryGrid }); const thumbnailSrc = 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 thumbnail = util.createElement('img', { className: 'media-thumbnail', attributes: { loading: 'lazy', src: thumbnailSrc }, parent: item }); const typeIcon = util.createElement('div', { className: 'media-type-icon', text: media.type === 'VIDEO' ? 'VID' : media.type === 'AUDIO' ? 'AUD' : 'IMG', parent: item }); item.addEventListener('click', () => this.showLightbox(media, index)); }); }, showLightbox(media, index) { this.currentIndex = typeof index === 'number' ? index : this.mediaElements.indexOf(media); this.updateLightboxContent(); this.lightbox.style.display = 'block'; }, updateLightboxContent() { const media = this.mediaElements[this.currentIndex]; let content; // Create appropriate element based on media type if (media.type === 'AUDIO') { content = util.createElement('audio', { className: 'lightbox-content', attributes: { controls: true, src: media.url } }); } else if (media.type === 'VIDEO') { content = util.createElement('video', { className: 'lightbox-content lightbox-video', attributes: { controls: true, src: media.url, autoplay: true, loop: true } }); } else { content = util.createElement('img', { className: 'lightbox-content', attributes: { src: media.url, loading: 'eager' } }); } // Remove existing content this.lightbox.querySelector('.lightbox-content')?.remove(); this.lightbox.querySelector('.go-to-post-btn')?.remove(); // Add "Go to post" button const goToPostBtn = util.createElement('button', { className: 'go-to-post-btn', text: 'Go to post', events: { click: () => { this.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); } } }); this.lightbox.appendChild(content); this.lightbox.appendChild(goToPostBtn); }, navigate(direction) { this.currentIndex = (this.currentIndex + direction + this.mediaElements.length) % this.mediaElements.length; this.updateLightboxContent(); }, updateThreadInfoDisplay() { const postCount = document.getElementById('postCount')?.textContent || '0'; const userCount = document.getElementById('userCountLabel')?.textContent || '0'; const fileCount = document.getElementById('fileCount')?.textContent || '0'; this.mediaInfoDisplay.textContent = `Posts: ${postCount} | Users: ${userCount} | Files: ${fileCount}`; } }; // SCROLL POSITION MEMORY // ============================== const scrollMemory = { currentPage: window.location.href, initialize() { window.addEventListener('beforeunload', () => this.saveScrollPosition()); window.addEventListener('load', () => this.restoreScrollPosition()); }, isExcludedPage(url) { return CONFIG.scrollMemory.excludedPatterns.some(pattern => pattern.test(url)); }, saveScrollPosition() { if (this.isExcludedPage(this.currentPage)) return; const scrollPosition = window.scrollY; localStorage.setItem(`scrollPosition_${this.currentPage}`, scrollPosition); this.manageScrollStorage(); }, restoreScrollPosition() { const savedPosition = localStorage.getItem(`scrollPosition_${this.currentPage}`); if (savedPosition) { window.scrollTo(0, parseInt(savedPosition, 10)); } }, manageScrollStorage() { const keys = Object.keys(localStorage).filter(key => key.startsWith('scrollPosition_')); if (keys.length > CONFIG.scrollMemory.maxPages) { keys.sort((a, b) => localStorage.getItem(a) - localStorage.getItem(b)); while (keys.length > CONFIG.scrollMemory.maxPages) { localStorage.removeItem(keys.shift()); } } } }; // BOARD NAVIGATION ENHANCER // ============================== const boardNavigation = { initialize() { this.appendCatalogToLinks(); // Watch for changes in the navigation bar const navboardsSpan = document.getElementById('navBoardsSpan'); if (navboardsSpan) { const observer = new MutationObserver(() => this.appendCatalogToLinks()); observer.observe(navboardsSpan, { childList: true, subtree: true }); } }, appendCatalogToLinks() { const navboardsSpan = document.getElementById('navBoardsSpan'); if (!navboardsSpan) return; const links = navboardsSpan.getElementsByTagName('a'); for (let link of links) { if (link.href && !link.href.endsWith('/catalog.html')) { link.href += '/catalog.html'; } } } }; // IMAGE HOVER FIX // ============================== const imageHoverFix = { initialize() { const observer = new MutationObserver(mutations => { mutations.forEach(mutation => { mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE && node.matches('img[style*="position: fixed"]')) { document.addEventListener('mousemove', this.handleMouseMove); } }); mutation.removedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE && node.matches('img[style*="position: fixed"]')) { document.removeEventListener('mousemove', this.handleMouseMove); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); }, handleMouseMove(event) { const img = document.querySelector('img[style*="position: fixed"]'); if (!img) return; const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; let newX = event.clientX + 10; let newY = event.clientY + 10; if (newX + img.width > viewportWidth) { newX = viewportWidth - img.width - 10; } if (newY + img.height > viewportHeight) { newY = viewportHeight - img.height - 10; } img.style.left = `${newX}px`; img.style.top = `${newY}px`; } }; // INITIALIZATION // ============================== function init() { // Apply styles if (typeof GM_addStyle === 'function') { GM_addStyle(STYLES); } else if (typeof GM?.addStyle === 'function') { GM.addStyle(STYLES); } else { const style = document.createElement('style'); style.textContent = STYLES; document.head.appendChild(style); } // Initialize features if (util.isThreadPage()) { gallery.initialize(); } boardNavigation.initialize(); scrollMemory.initialize(); imageHoverFix.initialize(); } // Run initialization when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();