- // ==UserScript==
- // @name Enhanced 8chan UI
- // @version 1.6.6
- // @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';
-
- // 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(`
- .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;
- }
-
- /* Additional CSS from second script */
- /* Cleanup */
- #footer,
- #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;
- }
- `);
-
- // 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 = `
- <div class="close-btn">×</div>
- <button class="lightbox-nav lightbox-prev">←</button>
- <button class="lightbox-nav lightbox-next">→</button>
- `;
- 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();
- }
- });
-
- // 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 });
- })();
-
- // Initialize main gallery functionality
- collectMedia();
- createGalleryItems();
- updateThreadInfoDisplay();
- setInterval(updateThreadInfoDisplay, 5000);
- })();