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