// ==UserScript== // @name 8chan Style Script // @namespace 8chanSS // @match *://8chan.moe/*/res/* // @match *://8chan.se/*/res/* // @match *://8chan.cc/*/res/* // @match *://8chan.moe/*/catalog.html // @match *://8chan.se/*/catalog.html // @match *://8chan.cc/*/catalog.html // @grant none // @version 1.20 // @author OtakuDude // @run-at document-idle // @description Script to style 8chan // @license MIT // @downloadURL none // ==/UserScript== (function () { // --- Settings --- const scriptSettings = { // Organize settings by category site: { enableHeaderCatalogLinks: { label: "Header Catalog Links", default: true, subOptions: { openInNewTab: { label: "Always open in new tab", default: false } } }, enableBottomHeader: { label: "Bottom Header", default: false }, enableScrollSave: { label: "Save Scroll Position", default: true }, enableScrollArrows: { label: "Show Up/Down Arrows", default: false }, hoverVideoVolume: { label: "Hover Video Volume (0-100%)", default: 50, type: "number", min: 0, max: 100 }, }, threads: { beepOnYou: { label: "Beep on (You)", default: false }, notifyOnYou: { label: "Notify when (You) (!)", default: true }, blurSpoilers: { label: "Blur Spoilers", default: false, subOptions: { removeSpoilers: { label: "Remove Spoilers", default: false } } }, enableSaveName: { label: "Save Name Checkbox", default: true }, enableThreadImageHover: { label: "Thread Image Hover", default: true }, }, catalog: { enableCatalogImageHover: { label: "Catalog Image Hover", default: true }, }, styling: { enableStickyQR: { label: "Enable Sticky Quick Reply", default: false }, enableFitReplies: { label: "Fit Replies", default: false }, enableSidebar: { label: "Enable Sidebar", default: false }, hideAnnouncement: { label: "Hide Announcement", default: false }, hidePanelMessage: { label: "Hide Panel Message", default: false }, hidePostingForm: { label: "Hide Posting Form", default: false }, hideBanner: { label: "Hide Board Banners", default: false }, } }; // Flatten settings for backward compatibility with existing functions const flatSettings = {}; function flattenSettings() { Object.keys(scriptSettings).forEach(category => { Object.keys(scriptSettings[category]).forEach(key => { flatSettings[key] = scriptSettings[category][key]; // Also flatten any sub-options if (scriptSettings[category][key].subOptions) { Object.keys(scriptSettings[category][key].subOptions).forEach(subKey => { const fullKey = `${key}_${subKey}`; flatSettings[fullKey] = scriptSettings[category][key].subOptions[subKey]; }); } }); }); } flattenSettings(); function getSetting(key) { // Check if the key exists in flatSettings if (!flatSettings[key]) { console.warn(`Setting key not found: ${key}`); return false; // Default to false for unknown settings } const val = localStorage.getItem('8chanSS_' + key); if (val === null) return flatSettings[key].default; if (flatSettings[key].type === "number") return Number(val); return val === 'true'; } function setSetting(key, value) { localStorage.setItem('8chanSS_' + key, value); } // --- Menu Icon --- const themeSelector = document.getElementById('themesBefore'); let link = null; let bracketSpan = null; if (themeSelector) { bracketSpan = document.createElement('span'); bracketSpan.textContent = '] [ '; link = document.createElement('a'); link.id = '8chanSS-icon'; link.href = '#'; link.textContent = '8chanSS'; themeSelector.parentNode.insertBefore(bracketSpan, themeSelector.nextSibling); themeSelector.parentNode.insertBefore(link, bracketSpan.nextSibling); } // --- Floating Settings Menu with Tabs --- function createSettingsMenu() { let menu = document.getElementById('8chanSS-menu'); if (menu) return menu; menu = document.createElement('div'); menu.id = '8chanSS-menu'; menu.style.position = 'fixed'; menu.style.top = '80px'; menu.style.left = '30px'; menu.style.zIndex = 99999; menu.style.background = '#222'; menu.style.color = '#fff'; menu.style.padding = '0'; menu.style.borderRadius = '8px'; menu.style.boxShadow = '0 4px 16px rgba(0,0,0,0.25)'; menu.style.display = 'none'; menu.style.minWidth = '220px'; menu.style.width = '100%'; menu.style.maxWidth = '350px'; menu.style.fontFamily = 'sans-serif'; menu.style.userSelect = 'none'; // Draggable let isDragging = false, dragOffsetX = 0, dragOffsetY = 0; const header = document.createElement('div'); header.style.display = 'flex'; header.style.justifyContent = 'space-between'; header.style.alignItems = 'center'; header.style.marginBottom = '0'; header.style.cursor = 'move'; header.style.background = '#333'; header.style.padding = '5px 18px 5px'; header.style.borderTopLeftRadius = '8px'; header.style.borderTopRightRadius = '8px'; header.addEventListener('mousedown', function (e) { isDragging = true; const rect = menu.getBoundingClientRect(); dragOffsetX = e.clientX - rect.left; dragOffsetY = e.clientY - rect.top; document.body.style.userSelect = 'none'; }); document.addEventListener('mousemove', function (e) { if (!isDragging) return; let newLeft = e.clientX - dragOffsetX; let newTop = e.clientY - dragOffsetY; const menuRect = menu.getBoundingClientRect(); const menuWidth = menuRect.width; const menuHeight = menuRect.height; const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; newLeft = Math.max(0, Math.min(newLeft, viewportWidth - menuWidth)); newTop = Math.max(0, Math.min(newTop, viewportHeight - menuHeight)); menu.style.left = newLeft + 'px'; menu.style.top = newTop + 'px'; menu.style.right = 'auto'; }); document.addEventListener('mouseup', function () { isDragging = false; document.body.style.userSelect = ''; }); // Title and close button const title = document.createElement('span'); title.textContent = '8chanSS Settings'; title.style.fontWeight = 'bold'; header.appendChild(title); const closeBtn = document.createElement('button'); closeBtn.textContent = '✕'; closeBtn.style.background = 'none'; closeBtn.style.border = 'none'; closeBtn.style.color = '#fff'; closeBtn.style.fontSize = '18px'; closeBtn.style.cursor = 'pointer'; closeBtn.style.marginLeft = '10px'; closeBtn.addEventListener('click', () => { menu.style.display = 'none'; }); header.appendChild(closeBtn); menu.appendChild(header); // Tab navigation const tabNav = document.createElement('div'); tabNav.style.display = 'flex'; tabNav.style.borderBottom = '1px solid #444'; tabNav.style.background = '#2a2a2a'; // Tab content container const tabContent = document.createElement('div'); tabContent.style.padding = '15px 18px'; tabContent.style.maxHeight = '60vh'; tabContent.style.overflowY = 'auto'; // Store current (unsaved) values const tempSettings = {}; Object.keys(flatSettings).forEach(key => { tempSettings[key] = getSetting(key); }); // Create tabs const tabs = { site: { label: 'Site', content: createTabContent('site', tempSettings) }, threads: { label: 'Threads', content: createTabContent('threads', tempSettings) }, catalog: { label: 'Catalog', content: createTabContent('catalog', tempSettings) }, styling: { label: 'Styling', content: createTabContent('styling', tempSettings) } }; // Create tab buttons Object.keys(tabs).forEach((tabId, index, arr) => { const tab = tabs[tabId]; const tabButton = document.createElement('button'); tabButton.textContent = tab.label; tabButton.dataset.tab = tabId; tabButton.style.background = index === 0 ? '#333' : 'transparent'; tabButton.style.border = 'none'; tabButton.style.borderRight = '1px solid #444'; tabButton.style.color = '#fff'; tabButton.style.padding = '8px 15px'; tabButton.style.margin = '5px 0 0 0'; tabButton.style.cursor = 'pointer'; tabButton.style.flex = '1'; tabButton.style.fontSize = '14px'; tabButton.style.transition = 'background 0.2s'; // Add rounded corners and margin to the first and last tab if (index === 0) { tabButton.style.borderTopLeftRadius = '8px'; tabButton.style.margin = '5px 0 0 5px'; } if (index === arr.length - 1) { tabButton.style.borderTopRightRadius = '8px'; tabButton.style.margin = '5px 5px 0 0'; tabButton.style.borderRight = 'none'; // Remove border on last tab } tabButton.addEventListener('click', () => { // Hide all tab contents Object.values(tabs).forEach(t => { t.content.style.display = 'none'; }); // Show selected tab content tab.content.style.display = 'block'; // Update active tab button tabNav.querySelectorAll('button').forEach(btn => { btn.style.background = 'transparent'; }); tabButton.style.background = '#333'; }); tabNav.appendChild(tabButton); }); menu.appendChild(tabNav); // Add all tab contents to the container Object.values(tabs).forEach((tab, index) => { tab.content.style.display = index === 0 ? 'block' : 'none'; tabContent.appendChild(tab.content); }); menu.appendChild(tabContent); // Button container for Save and Reset buttons const buttonContainer = document.createElement('div'); buttonContainer.style.display = 'flex'; buttonContainer.style.gap = '10px'; buttonContainer.style.padding = '0 18px 15px'; // Save Button const saveBtn = document.createElement('button'); saveBtn.textContent = 'Save'; saveBtn.style.background = '#4caf50'; saveBtn.style.color = '#fff'; saveBtn.style.border = 'none'; saveBtn.style.borderRadius = '4px'; saveBtn.style.padding = '8px 18px'; saveBtn.style.fontSize = '15px'; saveBtn.style.cursor = 'pointer'; saveBtn.style.flex = '1'; saveBtn.addEventListener('click', function () { Object.keys(tempSettings).forEach(key => { setSetting(key, tempSettings[key]); }); saveBtn.textContent = 'Saved!'; setTimeout(() => { saveBtn.textContent = 'Save'; }, 900); setTimeout(() => { window.location.reload(); }, 400); }); buttonContainer.appendChild(saveBtn); // Reset Button const resetBtn = document.createElement('button'); resetBtn.textContent = 'Reset'; resetBtn.style.background = '#dd3333'; resetBtn.style.color = '#fff'; resetBtn.style.border = 'none'; resetBtn.style.borderRadius = '4px'; resetBtn.style.padding = '8px 18px'; resetBtn.style.fontSize = '15px'; resetBtn.style.cursor = 'pointer'; resetBtn.style.flex = '1'; resetBtn.addEventListener('click', function () { if (confirm('Reset all 8chanSS settings to defaults?')) { // Find and remove all 8chanSS_ localStorage items Object.keys(localStorage).forEach(key => { if (key.startsWith('8chanSS_')) { localStorage.removeItem(key); } }); resetBtn.textContent = 'Reset!'; setTimeout(() => { resetBtn.textContent = 'Reset'; }, 900); setTimeout(() => { window.location.reload(); }, 400); } }); buttonContainer.appendChild(resetBtn); menu.appendChild(buttonContainer); // Info const info = document.createElement('div'); info.style.fontSize = '11px'; info.style.padding = '0 18px 12px'; info.style.opacity = '0.7'; info.style.textAlign = 'center'; info.textContent = 'Press Save to apply changes. Page will reload.'; menu.appendChild(info); document.body.appendChild(menu); return menu; } // Helper function to create tab content function createTabContent(category, tempSettings) { const container = document.createElement('div'); const categorySettings = scriptSettings[category]; Object.keys(categorySettings).forEach(key => { const setting = categorySettings[key]; // Parent row: flex for checkbox, label, chevron const parentRow = document.createElement('div'); parentRow.style.display = 'flex'; parentRow.style.alignItems = 'center'; parentRow.style.marginBottom = '0px'; // Special case: hoverVideoVolume slider if (key === "hoverVideoVolume" && setting.type === "number") { const label = document.createElement('label'); label.htmlFor = 'setting_' + key; label.textContent = setting.label + ': '; label.style.flex = '1'; const sliderContainer = document.createElement('div'); sliderContainer.style.display = 'flex'; sliderContainer.style.alignItems = 'center'; sliderContainer.style.flex = '1'; const slider = document.createElement('input'); slider.type = 'range'; slider.id = 'setting_' + key; slider.min = setting.min; slider.max = setting.max; slider.value = tempSettings[key]; slider.style.flex = 'unset'; slider.style.width = '100px'; slider.style.marginRight = '10px'; const valueLabel = document.createElement('span'); valueLabel.textContent = slider.value + '%'; valueLabel.style.minWidth = '40px'; valueLabel.style.textAlign = 'right'; slider.addEventListener('input', function () { let val = Number(slider.value); if (isNaN(val)) val = setting.default; val = Math.max(setting.min, Math.min(setting.max, val)); slider.value = val; tempSettings[key] = val; valueLabel.textContent = val + '%'; }); sliderContainer.appendChild(slider); sliderContainer.appendChild(valueLabel); parentRow.appendChild(label); parentRow.appendChild(sliderContainer); // Wrapper for parent row and sub-options const wrapper = document.createElement('div'); wrapper.style.marginBottom = '10px'; wrapper.appendChild(parentRow); container.appendChild(wrapper); return; // Skip the rest for this key } // Checkbox for boolean settings const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.id = 'setting_' + key; checkbox.checked = tempSettings[key]; checkbox.style.marginRight = '8px'; // Label const label = document.createElement('label'); label.htmlFor = checkbox.id; label.textContent = setting.label; label.style.flex = '1'; // Chevron for subOptions let chevron = null; let subOptionsContainer = null; if (setting.subOptions) { chevron = document.createElement('span'); chevron.className = 'ss-chevron'; chevron.innerHTML = '▶'; // Right-pointing triangle chevron.style.display = 'inline-block'; chevron.style.transition = 'transform 0.2s'; chevron.style.marginLeft = '6px'; chevron.style.fontSize = '12px'; chevron.style.userSelect = 'none'; chevron.style.transform = checkbox.checked ? 'rotate(90deg)' : 'rotate(0deg)'; } // Checkbox change handler checkbox.addEventListener('change', function () { tempSettings[key] = checkbox.checked; if (setting.subOptions && subOptionsContainer) { subOptionsContainer.style.display = checkbox.checked ? 'block' : 'none'; if (chevron) { chevron.style.transform = checkbox.checked ? 'rotate(90deg)' : 'rotate(0deg)'; } } }); parentRow.appendChild(checkbox); parentRow.appendChild(label); if (chevron) parentRow.appendChild(chevron); // Wrapper for parent row and sub-options const wrapper = document.createElement('div'); wrapper.style.marginBottom = '10px'; wrapper.appendChild(parentRow); // Handle sub-options if any exist if (setting.subOptions) { subOptionsContainer = document.createElement('div'); subOptionsContainer.style.marginLeft = '25px'; subOptionsContainer.style.marginTop = '5px'; subOptionsContainer.style.display = checkbox.checked ? 'block' : 'none'; Object.keys(setting.subOptions).forEach(subKey => { const subSetting = setting.subOptions[subKey]; const fullKey = `${key}_${subKey}`; const subWrapper = document.createElement('div'); subWrapper.style.marginBottom = '5px'; const subCheckbox = document.createElement('input'); subCheckbox.type = 'checkbox'; subCheckbox.id = 'setting_' + fullKey; subCheckbox.checked = tempSettings[fullKey]; subCheckbox.style.marginRight = '8px'; subCheckbox.addEventListener('change', function () { tempSettings[fullKey] = subCheckbox.checked; }); const subLabel = document.createElement('label'); subLabel.htmlFor = subCheckbox.id; subLabel.textContent = subSetting.label; subWrapper.appendChild(subCheckbox); subWrapper.appendChild(subLabel); subOptionsContainer.appendChild(subWrapper); }); wrapper.appendChild(subOptionsContainer); } container.appendChild(wrapper); }); // Add minimal CSS for chevron (only once) if (!document.getElementById('ss-chevron-style')) { const style = document.createElement('style'); style.id = 'ss-chevron-style'; style.textContent = ` .ss-chevron { transition: transform 0.2s; margin-left: 6px; font-size: 12px; display: inline-block; } `; document.head.appendChild(style); } return container; } // Hook up the icon to open/close the menu if (link) { let menu = createSettingsMenu(); link.style.cursor = 'pointer'; link.title = 'Open 8chanSS settings'; link.addEventListener('click', function (e) { e.preventDefault(); menu = createSettingsMenu(); menu.style.display = (menu.style.display === 'none') ? 'block' : 'none'; }); } /* --- Scroll Arrows Feature --- */ function featureScrollArrows() { // Only add once if (document.getElementById('scroll-arrow-up') || document.getElementById('scroll-arrow-down')) return; // Up arrow const upBtn = document.createElement('button'); upBtn.id = 'scroll-arrow-up'; upBtn.className = 'scroll-arrow-btn'; upBtn.title = 'Scroll to top'; upBtn.innerHTML = '▲'; upBtn.addEventListener('click', () => { window.scrollTo({ top: 0, behavior: 'smooth' }); }); // Down arrow const downBtn = document.createElement('button'); downBtn.id = 'scroll-arrow-down'; downBtn.className = 'scroll-arrow-btn'; downBtn.title = 'Scroll to bottom'; downBtn.innerHTML = '▼'; downBtn.addEventListener('click', () => { const footer = document.getElementById('footer'); if (footer) { footer.scrollIntoView({ behavior: 'smooth', block: 'end' }); } else { window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }); } }); document.body.appendChild(upBtn); document.body.appendChild(downBtn); } // --- Feature: Beep on (You) --- function featureBeepOnYou() { // Beep sound (base64) const beep = new Audio('data:audio/wav;base64,UklGRjQDAABXQVZFZm10IBAAAAABAAEAgD4AAIA+AAABAAgAc21wbDwAAABBAAADAAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABkYXRhzAIAAGMms8em0tleMV4zIpLVo8nhfSlcPR102Ki+5JspVEkdVtKzs+K1NEhUIT7DwKrcy0g6WygsrM2k1NpiLl0zIY/WpMrjgCdbPhxw2Kq+5Z4qUkkdU9K1s+K5NkVTITzBwqnczko3WikrqM+l1NxlLF0zIIvXpsnjgydZPhxs2ay95aIrUEkdUdC3suK8N0NUIjq+xKrcz002WioppdGm091pK1w0IIjYp8jkhydXPxxq2K295aUrTkoeTs65suK+OUFUIzi7xqrb0VA0WSoootKm0t5tKlo1H4TYqMfkiydWQBxm16+85actTEseS8y7seHAPD9TIza5yKra01QyWSson9On0d5wKVk2H4DYqcfkjidUQB1j1rG75KsvSkseScu8seDCPz1TJDW2yara1FYxWSwnm9Sn0N9zKVg2H33ZqsXkkihSQR1g1bK65K0wSEsfR8i+seDEQTxUJTOzy6rY1VowWC0mmNWoz993KVc3H3rYq8TklSlRQh1d1LS647AyR0wgRMbAsN/GRDpTJTKwzKrX1l4vVy4lldWpzt97KVY4IXbUr8LZljVPRCxhw7W3z6ZISkw1VK+4sMWvXEhSPk6buay9sm5JVkZNiLWqtrJ+TldNTnquqbCwilZXU1BwpKirrpNgWFhTaZmnpquZbFlbVmWOpaOonHZcXlljhaGhpZ1+YWBdYn2cn6GdhmdhYGN3lp2enIttY2Jjco+bnJuOdGZlZXCImJqakHpoZ2Zug5WYmZJ/bGlobX6RlpeSg3BqaW16jZSVkoZ0bGtteImSk5KIeG5tbnaFkJKRinxxbm91gY2QkIt/c3BwdH6Kj4+LgnZxcXR8iI2OjIR5c3J0e4WLjYuFe3VzdHmCioyLhn52dHR5gIiKioeAeHV1eH+GiYqHgXp2dnh9hIiJh4J8eHd4fIKHiIeDfXl4eHyBhoeHhH96eHmA'); // Store the original title const originalTitle = document.title; let isNotifying = false; // Create MutationObserver to detect when you are quoted const observer = new MutationObserver(mutations => { mutations.forEach(mutation => { mutation.addedNodes.forEach(node => { if (node.nodeType === 1 && node.querySelector && node.querySelector('a.quoteLink.you')) { // Only play beep if the setting is enabled if (getSetting('beepOnYou')) { playBeep(); } // Trigger notification in separate function if enabled if (getSetting('notifyOnYou')) { featureNotifyOnYou(); } } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); // Function to play the beep sound function playBeep() { if (beep.paused) { beep.play().catch(e => console.warn("Beep failed:", e)); } else { beep.addEventListener('ended', () => beep.play(), { once: true }); } } // Function to notify on (You) function featureNotifyOnYou() { // Store the original title if not already stored if (!window.originalTitle) { window.originalTitle = document.title; } // Add notification to title if not already notifying and tab not focused if (!window.isNotifying && !document.hasFocus()) { window.isNotifying = true; document.title = "(!) " + window.originalTitle; // Set up focus event listener if not already set if (!window.notifyFocusListenerAdded) { window.addEventListener('focus', () => { if (window.isNotifying) { document.title = window.originalTitle; window.isNotifying = false; } }); window.notifyFocusListenerAdded = true; } } } // Function to add notification to the title function addNotificationToTitle() { if (!isNotifying && !document.hasFocus()) { isNotifying = true; document.title = "(!) " + originalTitle; } } // Remove notification when tab regains focus window.addEventListener('focus', () => { if (isNotifying) { document.title = originalTitle; isNotifying = false; } }); } // --- Feature: Header Catalog Links --- function featureHeaderCatalogLinks() { function appendCatalogToLinks() { const navboardsSpan = document.getElementById('navBoardsSpan'); if (navboardsSpan) { const links = navboardsSpan.getElementsByTagName('a'); const openInNewTab = getSetting('enableHeaderCatalogLinks_openInNewTab'); for (let link of links) { if (link.href && !link.href.endsWith('/catalog.html')) { link.href += '/catalog.html'; // Set target="_blank" if the option is enabled if (openInNewTab) { link.target = '_blank'; link.rel = 'noopener noreferrer'; // Security best practice } else { link.target = ''; link.rel = ''; } } } } } appendCatalogToLinks(); const observer = new MutationObserver(appendCatalogToLinks); const config = { childList: true, subtree: true }; const navboardsSpan = document.getElementById('navBoardsSpan'); if (navboardsSpan) { observer.observe(navboardsSpan, config); } } // --- Feature: Save Scroll Position --- function featureSaveScrollPosition() { const MAX_PAGES = 50; const currentPage = window.location.href; const excludedPagePatterns = [ /\/catalog\.html$/i, ]; function isExcludedPage(url) { return excludedPagePatterns.some(pattern => pattern.test(url)); } function saveScrollPosition() { if (isExcludedPage(currentPage)) return; const scrollPosition = window.scrollY; localStorage.setItem(`8chanSS_scrollPosition_${currentPage}`, scrollPosition); manageScrollStorage(); } function restoreScrollPosition() { // If the URL contains a hash (e.g. /res/1190.html#1534), do nothing if (window.location.hash && window.location.hash.length > 1) { // There is a hash fragment, skip restoring scroll position return; } const savedPosition = localStorage.getItem(`8chanSS_scrollPosition_${currentPage}`); if (savedPosition) { window.scrollTo(0, parseInt(savedPosition, 10)); } } function manageScrollStorage() { const keys = Object.keys(localStorage).filter(key => key.startsWith('8chanSS_scrollPosition_')); if (keys.length > MAX_PAGES) { keys.sort((a, b) => { return localStorage.getItem(a) - localStorage.getItem(b); }); while (keys.length > MAX_PAGES) { localStorage.removeItem(keys.shift()); } } } window.addEventListener('beforeunload', saveScrollPosition); window.addEventListener('load', restoreScrollPosition); } // --- Feature: Catalog & Image Hover --- function featureImageHover() { function getFullMediaSrcFromMime(thumbnailSrc, filemime) { if (!thumbnailSrc || !filemime) return null; let base = thumbnailSrc.replace(/\/t_/, '/'); base = base.replace(/\.(jpe?g|png|gif|webp|webm|mp4)$/i, ''); const mimeToExt = { 'image/jpeg': '.jpg', 'image/jpg': '.jpg', 'image/png': '.png', 'image/gif': '.gif', 'image/webp': '.webp', 'video/mp4': '.mp4', 'video/webm': '.webm' }; const ext = mimeToExt[filemime.toLowerCase()]; if (!ext) return null; return base + ext; } let floatingMedia = null; let removeListeners = null; let hoverTimeout = null; let lastThumb = null; let isStillHovering = false; function cleanupFloatingMedia() { if (hoverTimeout) { clearTimeout(hoverTimeout); hoverTimeout = null; } if (removeListeners) { removeListeners(); removeListeners = null; } if (floatingMedia) { if (floatingMedia.tagName === 'VIDEO') { try { floatingMedia.pause(); floatingMedia.removeAttribute('src'); floatingMedia.load(); } catch (e) { } } if (floatingMedia.parentNode) { floatingMedia.parentNode.removeChild(floatingMedia); } } floatingMedia = null; lastThumb = null; isStillHovering = false; document.removeEventListener('mousemove', onMouseMove); } function onMouseMove(event) { if (!floatingMedia) return; const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; let mediaWidth = 0, mediaHeight = 0; if (floatingMedia.tagName === 'IMG') { mediaWidth = floatingMedia.naturalWidth || floatingMedia.width || floatingMedia.offsetWidth || 0; mediaHeight = floatingMedia.naturalHeight || floatingMedia.height || floatingMedia.offsetHeight || 0; } else if (floatingMedia.tagName === 'VIDEO') { mediaWidth = floatingMedia.videoWidth || floatingMedia.offsetWidth || 0; mediaHeight = floatingMedia.videoHeight || floatingMedia.offsetHeight || 0; } mediaWidth = Math.min(mediaWidth, viewportWidth * 0.9); mediaHeight = Math.min(mediaHeight, viewportHeight * 0.9); let newX = event.clientX + 10; let newY = event.clientY + 10; if (newX + mediaWidth > viewportWidth) { newX = viewportWidth - mediaWidth - 10; } if (newY + mediaHeight > viewportHeight) { newY = viewportHeight - mediaHeight - 10; } newX = Math.max(newX, 0); newY = Math.max(newY, 0); floatingMedia.style.left = `${newX}px`; floatingMedia.style.top = `${newY}px`; floatingMedia.style.maxWidth = '90vw'; floatingMedia.style.maxHeight = '90vh'; } function onThumbEnter(e) { const thumb = e.currentTarget; // Debounce: if already hovering this thumb, do nothing if (lastThumb === thumb) return; lastThumb = thumb; // Clean up any previous floating media and debounce cleanupFloatingMedia(); isStillHovering = true; // Listen for mouseleave to cancel hover if left before timeout function onLeave() { isStillHovering = false; cleanupFloatingMedia(); } thumb.addEventListener('mouseleave', onLeave, { once: true }); // Debounce: wait a short time before showing preview hoverTimeout = setTimeout(() => { hoverTimeout = null; // If mouse has left before timeout, do not show preview if (!isStillHovering) return; const parentA = thumb.closest('a.linkThumb, a.imgLink'); if (!parentA) return; const filemime = parentA.getAttribute('data-filemime'); const fullSrc = getFullMediaSrcFromMime(thumb.getAttribute('src'), filemime); if (!fullSrc) return; let loaded = false; function setCommonStyles(el) { el.style.position = 'fixed'; el.style.zIndex = 9999; el.style.pointerEvents = 'none'; el.style.maxWidth = '95vw'; el.style.maxHeight = '95vh'; el.style.transition = 'opacity 0.15s'; el.style.opacity = '0'; el.style.left = '-9999px'; } // Setup cleanup listeners removeListeners = function () { window.removeEventListener('scroll', cleanupFloatingMedia, true); }; window.addEventListener('scroll', cleanupFloatingMedia, true); if (filemime && filemime.startsWith('image/')) { floatingMedia = document.createElement('img'); setCommonStyles(floatingMedia); floatingMedia.onload = function () { if (!loaded && floatingMedia && isStillHovering) { loaded = true; floatingMedia.style.opacity = '1'; document.body.appendChild(floatingMedia); document.addEventListener('mousemove', onMouseMove); onMouseMove(e); } }; floatingMedia.onerror = cleanupFloatingMedia; floatingMedia.src = fullSrc; } else if (filemime && filemime.startsWith('video/')) { floatingMedia = document.createElement('video'); setCommonStyles(floatingMedia); floatingMedia.autoplay = true; floatingMedia.loop = true; floatingMedia.muted = false; floatingMedia.playsInline = true; // Set volume from settings (0-100) let volume = typeof getSetting === "function" ? getSetting('hoverVideoVolume') : 50; if (typeof volume !== 'number' || isNaN(volume)) volume = 50; floatingMedia.volume = Math.max(0, Math.min(1, volume / 100)); floatingMedia.onloadeddata = function () { if (!loaded && floatingMedia && isStillHovering) { loaded = true; floatingMedia.style.opacity = '1'; document.body.appendChild(floatingMedia); document.addEventListener('mousemove', onMouseMove); onMouseMove(e); } }; floatingMedia.onerror = cleanupFloatingMedia; floatingMedia.src = fullSrc; } }, 120); // 120ms debounce for both images and videos } function attachThumbListeners(root) { const thumbs = (root || document).querySelectorAll('a.linkThumb > img, a.imgLink > img'); thumbs.forEach(thumb => { if (!thumb._fullImgHoverBound) { thumb.addEventListener('mouseenter', onThumbEnter); thumb._fullImgHoverBound = true; } }); } attachThumbListeners(); const observer = new MutationObserver(mutations => { mutations.forEach(mutation => { mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) { attachThumbListeners(node); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); } // --- Feature: Save Name Checkbox --- function featureSaveNameCheckbox() { const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.id = 'saveNameCheckbox'; checkbox.classList.add('postingCheckbox'); const label = document.createElement('label'); label.htmlFor = 'saveNameCheckbox'; label.textContent = 'Save Name'; label.title = 'Save Name on refresh'; const alwaysUseBypassCheckbox = document.getElementById('qralwaysUseBypassCheckBox'); if (alwaysUseBypassCheckbox) { alwaysUseBypassCheckbox.parentNode.insertBefore(checkbox, alwaysUseBypassCheckbox); alwaysUseBypassCheckbox.parentNode.insertBefore(label, checkbox.nextSibling); const savedCheckboxState = localStorage.getItem('8chanSS_saveNameCheckbox') === 'true'; checkbox.checked = savedCheckboxState; const nameInput = document.getElementById('qrname'); if (nameInput) { const savedName = localStorage.getItem('name'); if (checkbox.checked && savedName !== null) { nameInput.value = savedName; } else if (!checkbox.checked) { nameInput.value = ''; } nameInput.addEventListener('input', function () { if (checkbox.checked) { localStorage.setItem('name', nameInput.value); } }); checkbox.addEventListener('change', function () { if (checkbox.checked) { localStorage.setItem('name', nameInput.value); } else { localStorage.removeItem('name'); nameInput.value = ''; } localStorage.setItem('8chanSS_saveNameCheckbox', checkbox.checked); }); } } } /* --- Feature: Blur Spoilers + Remove Spoilers suboption --- */ function featureBlurSpoilers() { function revealSpoilers() { const spoilerLinks = document.querySelectorAll('a.imgLink'); spoilerLinks.forEach(link => { const img = link.querySelector('img'); if (img && !img.src.includes('/.media/t_')) { let href = link.getAttribute('href'); if (href) { // Extract filename without extension const match = href.match(/\/\.media\/([^\/]+)\.[a-zA-Z0-9]+$/); if (match) { // Use the thumbnail path (t_filename) const transformedSrc = `/\.media/t_${match[1]}`; img.src = transformedSrc; // If Remove Spoilers is enabled, do not apply blur, just show the thumbnail if (getSetting('blurSpoilers_removeSpoilers')) { img.style.filter = ''; img.style.transition = ''; img.onmouseover = null; img.onmouseout = null; return; } else { img.style.filter = 'blur(5px)'; img.style.transition = 'filter 0.3s ease'; img.addEventListener('mouseover', () => { img.style.filter = 'none'; }); img.addEventListener('mouseout', () => { img.style.filter = 'blur(5px)'; }); } } } } }); } // Initial run revealSpoilers(); // Observe for dynamically added spoilers const observer = new MutationObserver(revealSpoilers); observer.observe(document.body, { childList: true, subtree: true }); } // --- Feature: CSS Class Toggles --- function featureCssClassToggles() { // Map of setting keys to CSS class names const classToggles = { 'enableFitReplies': 'fit-replies', 'enableSidebar': 'ss-sidebar', 'enableStickyQR': 'sticky-qr', 'enableBottomHeader': 'bottom-header', 'hideBanner': 'disable-banner' // Add more class toggles here in the future }; // Process each toggle Object.entries(classToggles).forEach(([settingKey, className]) => { if (getSetting(settingKey)) { document.documentElement.classList.add(className); } else { document.documentElement.classList.remove(className); } }); } // --- Feature: Hide/Show Posting Form, Announcement, Panel Message --- function featureHideElements() { // These settings are: hidePostingForm, hideAnnouncement, hidePanelMessage const postingFormDiv = document.getElementById('postingForm'); const announcementDiv = document.getElementById('dynamicAnnouncement'); const panelMessageDiv = document.getElementById('panelMessage'); if (postingFormDiv) { postingFormDiv.style.display = getSetting('hidePostingForm') ? 'none' : ''; } if (announcementDiv) { announcementDiv.style.display = getSetting('hideAnnouncement') ? 'none' : ''; } if (panelMessageDiv) { panelMessageDiv.style.display = getSetting('hidePanelMessage') ? 'none' : ''; } } // --- Feature Initialization based on Settings --- if (getSetting('blurSpoilers')) { featureBlurSpoilers(); } if (getSetting('enableHeaderCatalogLinks')) { featureHeaderCatalogLinks(); } if (getSetting('enableScrollSave')) { featureSaveScrollPosition(); } if (getSetting('enableSaveName')) { featureSaveNameCheckbox(); } if (getSetting('enableScrollArrows')) { featureScrollArrows(); } if (getSetting('beepOnYou') || getSetting('notifyOnYou')) { featureBeepOnYou(); } // Check if we should enable image hover based on the current page const isCatalogPage = /\/catalog\.html$/.test(window.location.pathname.toLowerCase()); if ((isCatalogPage && getSetting('enableCatalogImageHover')) || (!isCatalogPage && getSetting('enableThreadImageHover'))) { featureImageHover(); } // Always run hide/show feature (it will respect settings) featureHideElements(); featureCssClassToggles(); ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Keyboard Shortcuts // QR (CTRL+Q) function toggleQR(event) { // Check if Ctrl + Q is pressed if (event.ctrlKey && (event.key === 'q' || event.key === 'Q')) { const hiddenDiv = document.getElementById('quick-reply'); // Toggle QR if (hiddenDiv.style.display === 'none' || hiddenDiv.style.display === '') { hiddenDiv.style.display = 'block'; // Show the div // Focus the textarea after a small delay to ensure it's visible setTimeout(() => { const textarea = document.getElementById('qrbody'); if (textarea) { textarea.focus(); } }, 50); } else { hiddenDiv.style.display = 'none'; // Hide the div } } } document.addEventListener('keydown', toggleQR); // Clear textarea and hide quick-reply on Escape key function clearTextarea(event) { // Check if Escape key is pressed if (event.key === 'Escape') { // Clear the textarea const textarea = document.getElementById('qrbody'); if (textarea) { textarea.value = ''; // Clear the textarea } // Hide the quick-reply div const quickReply = document.getElementById('quick-reply'); if (quickReply) { quickReply.style.display = 'none'; // Hide the quick-reply } } } document.addEventListener('keydown', clearTextarea); // Tags const bbCodeCombinations = new Map([ ["s", ["[spoiler]", "[/spoiler]"]], ["b", ["'''", "'''"]], ["u", ["__", "__"]], ["i", ["''", "''"]], ["d", ["[doom]", "[/doom]"]], ["m", ["[moe]", "[/moe]"]], ["c", ["[code]", "[/code]"]], ]); function replyKeyboardShortcuts(ev) { const key = ev.key.toLowerCase(); // Special case: alt+c for [code] tag if (key === "c" && ev.altKey && !ev.ctrlKey && bbCodeCombinations.has(key)) { ev.preventDefault(); const textBox = ev.target; const [openTag, closeTag] = bbCodeCombinations.get(key); const { selectionStart, selectionEnd, value } = textBox; if (selectionStart === selectionEnd) { // No selection: insert empty tags and place cursor between them const before = value.slice(0, selectionStart); const after = value.slice(selectionEnd); const newCursor = selectionStart + openTag.length; textBox.value = before + openTag + closeTag + after; textBox.selectionStart = textBox.selectionEnd = newCursor; } else { // Replace selected text with tags around it const before = value.slice(0, selectionStart); const selected = value.slice(selectionStart, selectionEnd); const after = value.slice(selectionEnd); textBox.value = before + openTag + selected + closeTag + after; // Keep selection around the newly wrapped text textBox.selectionStart = selectionStart + openTag.length; textBox.selectionEnd = selectionEnd + openTag.length; } return; } // All other tags: ctrl+key if (ev.ctrlKey && !ev.altKey && bbCodeCombinations.has(key) && key !== "c") { ev.preventDefault(); const textBox = ev.target; const [openTag, closeTag] = bbCodeCombinations.get(key); const { selectionStart, selectionEnd, value } = textBox; if (selectionStart === selectionEnd) { // No selection: insert empty tags and place cursor between them const before = value.slice(0, selectionStart); const after = value.slice(selectionEnd); const newCursor = selectionStart + openTag.length; textBox.value = before + openTag + closeTag + after; textBox.selectionStart = textBox.selectionEnd = newCursor; } else { // Replace selected text with tags around it const before = value.slice(0, selectionStart); const selected = value.slice(selectionStart, selectionEnd); const after = value.slice(selectionEnd); textBox.value = before + openTag + selected + closeTag + after; // Keep selection around the newly wrapped text textBox.selectionStart = selectionStart + openTag.length; textBox.selectionEnd = selectionEnd + openTag.length; } return; } } document.getElementById("qrbody")?.addEventListener("keydown", replyKeyboardShortcuts); ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Custom CSS injection function addCustomCSS(css) { if (!css) return; const style = document.createElement('style'); style.type = 'text/css'; style.appendChild(document.createTextNode(css)); document.head.appendChild(style); } // Get the current URL path const currentPath = window.location.pathname.toLowerCase(); const currentHost = window.location.hostname.toLowerCase(); // Apply CSS based on URL pattern // Thread page CSS if (/\/res\/[^/]+\.html$/.test(currentPath)) { const css = ` /* Quick Reply */ :root.sticky-qr #quick-reply { display: block; top: auto !important; bottom: 0; left: auto !important; position: fixed; right: 0 !important; } :root.bottom-header #quick-reply { bottom: 28px !important; } #quick-reply { padding: 0; opacity: 0.7; transition: opacity 0.3s ease; } #quick-reply:hover, #quick-reply:focus-within { opacity: 1; } #qrbody { resize: vertical; max-height: 50vh; height: 130px; } .floatingMenu { padding: 0 !important; } #qrFilesBody { max-width: 300px; } /* Banner */ :root.disable-banner #bannerImage { display: none; } :root.ss-sidebar #bannerImage { width: 305px; right: 0; position: fixed; top: 26px; } :root.ss-sidebar.bottom-header #bannerImage { top: 0 !important; } .innerUtility.top { margin-top: 2em; background-color: transparent !important; color: var(--link-color) !important; } .innerUtility.top a { color: var(--link-color) !important; } .quoteTooltip { z-index: 110; } /* (You) Replies */ .innerPost:has(.youName) { border-left: dashed #68b723 3px; } .innerPost:has(.quoteLink.you) { border-left: solid #dd003e 3px; } /* Filename & Thumbs */ .originalNameLink { display: inline; overflow-wrap: anywhere; white-space: normal; } .multipleUploads .uploadCell:not(.expandedCell) { max-width: 215px; } `; addCustomCSS(css); } if (/^8chan\.(se|moe)$/.test(currentHost)) { // General CSS for all pages const css = ` /* Margins */ body { margin: 0; } :root.ss-sidebar #mainPanel { margin-right: 305px; } /* Cleanup */ #navFadeEnd, #navFadeMid, #navTopBoardsSpan, .coloredIcon.linkOverboard, .coloredIcon.linkSfwOver, .coloredIcon.multiboardButton, #navLinkSpan>span:nth-child(9), #navLinkSpan>span:nth-child(11), #navLinkSpan>span:nth-child(13) { display: none; } footer { visibility: hidden; height: 0; } /* Header */ :not(:root.bottom-header) .navHeader { box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15); } :root.bottom-header nav.navHeader { top: auto !important; bottom: 0 !important; box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.15); } /* Thread Watcher */ #watchedMenu { font-size: smaller; padding: 5px !important; box-shadow: -3px 3px 2px 0px rgba(0,0,0,0.19); } #watchedMenu, #watchedMenu .floatingContainer { min-width: 200px; } #watchedMenu .watchedCellLabel > a:after { content: " - "attr(href); filter: saturate(50%); font-style: italic; font-weight: bold; } td.watchedCell > label.watchedCellLabel { text-overflow: ellipsis; overflow: hidden; white-space: nowrap; width: 180px; display: block; } td.watchedCell > label.watchedCellLabel:hover { overflow: unset; width: auto; white-space: normal; } /* Posts */ .quoteTooltip .innerPost { overflow: hidden; box-shadow: -3px 3px 2px 0px rgba(0,0,0,0.19); } :root.fit-replies .innerPost { margin-left: 10px; display: flow-root; } .scroll-arrow-btn { position: fixed; right: 50px; width: 36px; height: 35px; background: #222; color: #fff; border: none; border-radius: 50%; box-shadow: 0 2px 8px rgba(0,0,0,0.18); font-size: 22px; cursor: pointer; opacity: 0.7; z-index: 99998; display: flex; align-items: center; justify-content: center; transition: opacity 0.2s, background 0.2s; } :root.ss-sidebar .scroll-arrow-btn { right: 330px !important; } .scroll-arrow-btn:hover { opacity: 1; background: #444; } #scroll-arrow-up { bottom: 80px; } #scroll-arrow-down { bottom: 32px; } `; addCustomCSS(css); } // Catalog page CSS if (/\/catalog\.html$/.test(currentPath)) { const css = ` #dynamicAnnouncement { display: none; } #postingForm { margin: 2em auto; } `; addCustomCSS(css); } })();