// ==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
// ==/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);
})();