// ==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.16 // @author Anon // @run-at document-idle // @description Script to style 8chan // @license MIT // @downloadURL none // ==/UserScript== (function () { // --- Settings --- const scriptSettings = { beepOnYou: { label: "Beep on (You)", default: false }, watchThreadOnReply: { label: "Watch Thread On Reply", default: true }, enableScrollSave: { label: "Save Scroll Position", default: true }, enableScrollArrows: { label: "Show Up/Down Arrows", default: false }, blurSpoilers: { label: "Blur Spoilers", default: false }, enableHeaderCatalogLinks: { label: "Header Catalog Links", default: true }, enableCatalogImageHover: { label: "Catalog and Image Hover", default: true }, enableSaveName: { label: "Save Name checkbox", default: true }, enableFitReplies: { label: "Fit Replies", default: false }, hoverVideoVolume: { label: "Hover Video Volume (0-100%)", default: 50, type: "number", min: 0, max: 100 }, hidePostingForm: { label: "Hide Posting Form", default: false }, hideAnnouncement: { label: "Hide Announcement", default: false }, hidePanelMessage: { label: "Hide Panel Message", default: false } }; function getSetting(key) { const val = localStorage.getItem('8chanSS_' + key); if (val === null) return scriptSettings[key].default; if (scriptSettings[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 --- 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.right = '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 = '240px'; 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 = '2px'; 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); // Settings checkboxes and number/slider inputs const content = document.createElement('div'); content.style.padding = '18px 22px 18px 18px'; // Store current (unsaved) values const tempSettings = {}; Object.keys(scriptSettings).forEach(key => { tempSettings[key] = getSetting(key); }); Object.keys(scriptSettings).forEach(key => { const setting = scriptSettings[key]; const wrapper = document.createElement('div'); wrapper.style.marginBottom = '8px'; if (key === "hoverVideoVolume") { // Compact slider for hover video volume const label = document.createElement('label'); label.htmlFor = 'setting_' + key; label.textContent = setting.label + ': '; label.style.marginRight = '8px'; 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.verticalAlign = 'middle'; slider.style.marginRight = '6px'; slider.style.width = '80px'; // Compact width const valueLabel = document.createElement('span'); valueLabel.textContent = slider.value + '%'; valueLabel.style.display = 'inline-block'; valueLabel.style.minWidth = '32px'; 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 + '%'; }); wrapper.appendChild(label); wrapper.appendChild(slider); wrapper.appendChild(valueLabel); } else { // Checkbox for boolean settings const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.id = 'setting_' + key; checkbox.checked = tempSettings[key]; checkbox.style.marginRight = '8px'; checkbox.addEventListener('change', function () { tempSettings[key] = checkbox.checked; }); const label = document.createElement('label'); label.htmlFor = checkbox.id; label.textContent = setting.label; wrapper.appendChild(checkbox); wrapper.appendChild(label); } content.appendChild(wrapper); }); // Button container for Save and Reset buttons const buttonContainer = document.createElement('div'); buttonContainer.style.display = 'flex'; buttonContainer.style.gap = '10px'; buttonContainer.style.marginTop = '10px'; buttonContainer.style.marginBottom = '5px'; // 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); content.appendChild(buttonContainer); // Info const info = document.createElement('div'); info.style.fontSize = '11px'; info.style.marginTop = '12px'; info.style.opacity = '0.7'; info.textContent = 'Press Save to apply changes. Page will reload.'; content.appendChild(info); menu.appendChild(content); document.body.appendChild(menu); return menu; } // 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; // Styles for arrows const style = document.createElement('style'); style.textContent = ` .scroll-arrow-btn { position: fixed; right: 330px; 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; } .scroll-arrow-btn:hover { opacity: 1; background: #444; } #scroll-arrow-up { bottom: 80px; } #scroll-arrow-down { bottom: 32px; } `; document.head.appendChild(style); // 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 (same 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')) { playBeep(); addNotificationToTitle(); } }); }); }); 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 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: Fit Replies (CSS toggle) --- function featureFitReplies() { document.documentElement.classList.add('fit-replies'); if (!document.getElementById('fit-replies-style')) { const style = document.createElement('style'); style.id = 'fit-replies-style'; style.textContent = ` :root.fit-replies .innerPost { margin-left: 10px; display: flow-root; } `; document.head.appendChild(style); } } // --- Feature: Header Catalog Links --- function featureHeaderCatalogLinks() { 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'; } } } } appendCatalogToLinks(); const observer = new MutationObserver(appendCatalogToLinks); const config = { childList: true, subtree: true }; const navboardsSpan = document.getElementById('navBoardsSpan'); if (navboardsSpan) { observer.observe(navboardsSpan, config); } } // --- Watch Thread On Post --- thread.replyCallback = function (status, data) { if (status === 'ok') { postCommon.storeUsedPostingPassword(api.boardUri, api.threadId, data); api.addYou(api.boardUri, data); document.getElementById('fieldMessage').value = ''; document.getElementById('fieldSubject').value = ''; qr.clearQRAfterPosting(); postCommon.clearSelectedFiles(); //document.getElementById('footer').scrollIntoView(); if (!thread.autoRefresh || !thread.socket) { thread.refreshPosts(true); } // Only add to thread watcher if the setting is enabled if (getSetting('watchThreadOnReply')) { const watchButton = document.getElementsByClassName('watchButton')[0]; if (watchButton) { watchButton.click(); // Add to thread watcher on replying } } } else { alert(status + ': ' + JSON.stringify(data)); } }; // --- 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() { 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 (with video fix and volume setting) --- function featureCatalogImageHover() { 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 = '90vw'; el.style.maxHeight = '90vh'; 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 --- // --- 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('8chanSS_name'); if (checkbox.checked && savedName !== null) { nameInput.value = savedName; } else if (!checkbox.checked) { nameInput.value = ''; } nameInput.addEventListener('input', function () { if (checkbox.checked) { localStorage.setItem('8chanSS_name', nameInput.value); } }); checkbox.addEventListener('change', function () { if (checkbox.checked) { localStorage.setItem('8chanSS_name', nameInput.value); } else { localStorage.removeItem('8chanSS_name'); nameInput.value = ''; } localStorage.setItem('8chanSS_saveNameCheckbox', checkbox.checked); }); } } } /* --- Feature: Blur Spoilers --- */ 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; // Apply blur style img.style.filter = 'blur(5px)'; img.style.transition = 'filter 0.3s ease'; // Unblur on hover 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: 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('enableFitReplies')) { featureFitReplies(); } if (getSetting('blurSpoilers')) { featureBlurSpoilers(); } if (getSetting('enableHeaderCatalogLinks')) { featureHeaderCatalogLinks(); } if (getSetting('enableScrollSave')) { featureSaveScrollPosition(); } if (getSetting('enableCatalogImageHover')) { featureCatalogImageHover(); } if (getSetting('enableSaveName')) { featureSaveNameCheckbox(); } if (getSetting('enableScrollArrows')) { featureScrollArrows(); } if (getSetting('beepOnYou')) { featureBeepOnYou(); } // Always run hide/show feature (it will respect settings) featureHideElements(); ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // 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 } 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 */ #quick-reply { display: block; padding: 0 !important; top: auto !important; bottom: 0; left: auto !important; position: fixed; right: 0 !important; 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 */ #bannerImage { width: 305px; right: 0; position: fixed; top: 26px; } .innerUtility.top { margin-top: 2em; background-color: transparent !important; color: var(--link-color) !important; } .innerUtility.top a { color: var(--link-color) !important; } /* Hover Posts */ img[style*="position: fixed"] { max-width: 80vw; max-height: 80vh !important; z-index: 200; } .quoteTooltip { z-index: 110; } /* (You) Replies */ .innerPost:has(.youName) { border-left: solid #68b723 5px; } .innerPost:has(.quoteLink.you) { border-left: solid #dd003e 5px; } /* 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 */ #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 */ #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); } `; addCustomCSS(css); } // Catalog page CSS if (/\/catalog\.html$/.test(currentPath)) { const css = ` #dynamicAnnouncement { display: none; } #postingForm { margin: 2em auto; } `; addCustomCSS(css); } })();