- // ==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
- // ==/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: `
- <div class="close-btn">×</div>
- <button class="lightbox-nav lightbox-prev">←</button>
- <button class="lightbox-nav lightbox-next">→</button>
- `,
- 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();
- }
- })();