// ==UserScript==
// @name 8chan Style Script
// @namespace 8chanSS
// @match *://8chan.moe/*
// @match *://8chan.se/*
// @exclude *://8chan.moe/login.html
// @exclude *://8chan.se/login.html
// @grant GM.getValue
// @grant GM.setValue
// @grant GM.deleteValue
// @grant GM.listValues
// @version 1.25
// @author OtakuDude
// @run-at document-idle
// @description Script to style 8chan
// @license MIT
// @downloadURL none
// ==/UserScript==
(async function () {
/**
* Temporary: Remove all old 8chanSS_ keys from localStorage to not interfere with GM storage
*/
function cleanupOld8chanSSLocalStorage() {
try {
const keysToRemove = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (
key &&
(key.startsWith("8chanSS_") || key.startsWith("scrollPosition_"))
) {
keysToRemove.push(key);
}
}
keysToRemove.forEach((key) => localStorage.removeItem(key));
} catch (e) {
// Some browsers/extensions may restrict localStorage access
console.warn("8chanSS: Could not clean up old localStorage keys:", e);
}
}
// Call immediately at script start
cleanupOld8chanSSLocalStorage();
// --- 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 },
alwaysShowTW: { label: "Always Show Thread Watcher", 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,
subOptions: {
showCatalogForm: {
label: "Don't Hide in Catalog",
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();
// --- GM storage wrappers ---
async function getSetting(key) {
if (!flatSettings[key]) {
console.warn(`Setting key not found: ${key}`);
return false;
}
let val = await GM.getValue("8chanSS_" + key, null);
if (val === null) return flatSettings[key].default;
if (flatSettings[key].type === "number") return Number(val);
return val === "true";
}
async function setSetting(key, value) {
// Always store as string for consistency
await GM.setValue("8chanSS_" + key, String(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";
link.style.fontWeight = "bold";
themeSelector.parentNode.insertBefore(
bracketSpan,
themeSelector.nextSibling
);
themeSelector.parentNode.insertBefore(link, bracketSpan.nextSibling);
}
// --- Shortcuts tab ---
function createShortcutsTab() {
const container = document.createElement("div");
// Title
const title = document.createElement("h3");
title.textContent = "Keyboard Shortcuts";
title.style.margin = "0 0 15px 0";
title.style.fontSize = "16px";
container.appendChild(title);
// Shortcuts table
const table = document.createElement("table");
table.style.width = "100%";
table.style.borderCollapse = "collapse";
// Table styles
const tableStyles = {
th: {
textAlign: "left",
padding: "8px 5px",
borderBottom: "1px solid #444",
fontSize: "14px",
fontWeight: "bold",
},
td: {
padding: "8px 5px",
borderBottom: "1px solid #333",
fontSize: "13px",
},
kbd: {
background: "#333",
border: "1px solid #555",
borderRadius: "3px",
padding: "2px 5px",
fontSize: "12px",
fontFamily: "monospace",
},
};
// Create header row
const headerRow = document.createElement("tr");
const shortcutHeader = document.createElement("th");
shortcutHeader.textContent = "Shortcut";
Object.assign(shortcutHeader.style, tableStyles.th);
headerRow.appendChild(shortcutHeader);
const actionHeader = document.createElement("th");
actionHeader.textContent = "Action";
Object.assign(actionHeader.style, tableStyles.th);
headerRow.appendChild(actionHeader);
table.appendChild(headerRow);
// Shortcut data
const shortcuts = [
{ keys: ["Ctrl", "F1"], action: "Open 8chanSS settings" },
{ keys: ["Ctrl", "Q"], action: "Toggle Quick Reply" },
{ keys: ["Ctrl", "Enter"], action: "Submit post" },
{ keys: ["Escape"], action: "Clear textarea and hide Quick Reply" },
{ keys: ["Ctrl", "B"], action: "Bold text" },
{ keys: ["Ctrl", "I"], action: "Italic text" },
{ keys: ["Ctrl", "U"], action: "Underline text" },
{ keys: ["Ctrl", "S"], action: "Spoiler text" },
{ keys: ["Ctrl", "D"], action: "Doom text" },
{ keys: ["Ctrl", "M"], action: "Moe text" },
{ keys: ["Alt", "C"], action: "Code block" },
];
// Create rows for each shortcut
shortcuts.forEach((shortcut) => {
const row = document.createElement("tr");
// Shortcut cell
const shortcutCell = document.createElement("td");
Object.assign(shortcutCell.style, tableStyles.td);
// Create kbd elements for each key
shortcut.keys.forEach((key, index) => {
const kbd = document.createElement("kbd");
kbd.textContent = key;
Object.assign(kbd.style, tableStyles.kbd);
shortcutCell.appendChild(kbd);
// Add + between keys
if (index < shortcut.keys.length - 1) {
const plus = document.createTextNode(" + ");
shortcutCell.appendChild(plus);
}
});
row.appendChild(shortcutCell);
// Action cell
const actionCell = document.createElement("td");
actionCell.textContent = shortcut.action;
Object.assign(actionCell.style, tableStyles.td);
row.appendChild(actionCell);
table.appendChild(row);
});
container.appendChild(table);
// Add note about BBCode shortcuts
const note = document.createElement("p");
note.textContent =
"Text formatting shortcuts work when text is selected or when inserting at cursor position.";
note.style.fontSize = "12px";
note.style.marginTop = "15px";
note.style.opacity = "0.7";
note.style.fontStyle = "italic";
container.appendChild(note);
return container;
}
// --- Floating Settings Menu with Tabs ---
async 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 = "365px";
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 = {};
await Promise.all(
Object.keys(flatSettings).map(async (key) => {
tempSettings[key] = await 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: "Style",
content: createTabContent("styling", tempSettings),
},
shortcuts: {
label: "⌨️",
content: createShortcutsTab(),
},
};
// 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", async function () {
for (const key of Object.keys(tempSettings)) {
await 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", async function () {
if (confirm("Reset all 8chanSS settings to defaults?")) {
// Remove all 8chanSS_ GM values
const keys = await GM.listValues();
for (const key of keys) {
if (key.startsWith("8chanSS_")) {
await GM.deleteValue(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 = Number(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] === true || tempSettings[key] === "true";
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 = await createSettingsMenu();
link.style.cursor = "pointer";
link.title = "Open 8chanSS settings";
link.addEventListener("click", async function (e) {
e.preventDefault();
let menu = await 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(async (node) => {
if (
node.nodeType === 1 &&
node.querySelector &&
node.querySelector("a.quoteLink.you")
) {
// Only play beep if the setting is enabled
if (await getSetting("beepOnYou")) {
playBeep();
}
// Trigger notification in separate function if enabled
if (await 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 ---
async function featureHeaderCatalogLinks() {
async function appendCatalogToLinks() {
const navboardsSpan = document.getElementById("navBoardsSpan");
if (navboardsSpan) {
const links = navboardsSpan.getElementsByTagName("a");
const openInNewTab = await 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 ---
async 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));
}
async function saveScrollPosition() {
if (isExcludedPage(currentPage)) return;
const scrollPosition = window.scrollY;
const timestamp = Date.now();
// Store both the scroll position and timestamp using GM storage
await GM.setValue(
`8chanSS_scrollPosition_${currentPage}`,
JSON.stringify({
position: scrollPosition,
timestamp: timestamp,
})
);
await manageScrollStorage();
}
async function manageScrollStorage() {
// Get all GM storage keys
const allKeys = await GM.listValues();
// Filter for scroll position keys
const scrollKeys = allKeys.filter((key) =>
key.startsWith("8chanSS_scrollPosition_")
);
if (scrollKeys.length > MAX_PAGES) {
// Create array of objects with key and timestamp
const keyData = await Promise.all(
scrollKeys.map(async (key) => {
let data;
try {
const savedValue = await GM.getValue(key, null);
if (savedValue) {
data = JSON.parse(savedValue);
// Handle legacy format (just a number)
if (typeof data !== "object") {
data = { position: parseFloat(savedValue), timestamp: 0 };
}
} else {
data = { position: 0, timestamp: 0 };
}
} catch (e) {
// If parsing fails, assume it's old format
const savedValue = await GM.getValue(key, "0");
data = {
position: parseFloat(savedValue),
timestamp: 0,
};
}
return {
key: key,
timestamp: data.timestamp || 0,
};
})
);
// Sort by timestamp (oldest first)
keyData.sort((a, b) => a.timestamp - b.timestamp);
// Remove oldest entries until we're under the limit
const keysToRemove = keyData.slice(0, keyData.length - MAX_PAGES);
for (const item of keysToRemove) {
await GM.deleteValue(item.key);
}
}
}
async 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) {
return;
}
const savedData = await GM.getValue(
`8chanSS_scrollPosition_${currentPage}`,
null
);
if (savedData) {
let position;
try {
// Try to parse as JSON (new format)
const data = JSON.parse(savedData);
position = data.position;
// Update the timestamp to "refresh" this entry
await GM.setValue(
`8chanSS_scrollPosition_${currentPage}`,
JSON.stringify({
position: position,
timestamp: Date.now(),
})
);
} catch (e) {
// If parsing fails, assume it's the old format (just a number)
position = parseFloat(savedData);
// Convert to new format with current timestamp
await GM.setValue(
`8chanSS_scrollPosition_${currentPage}`,
JSON.stringify({
position: position,
timestamp: Date.now(),
})
);
}
if (!isNaN(position)) {
window.scrollTo(0, position);
}
}
}
// Use async event handlers
window.addEventListener("beforeunload", () => {
// We can't await in beforeunload, so we just call the function
saveScrollPosition();
});
// For load event, we can use an async function
window.addEventListener("load", async () => {
await restoreScrollPosition();
});
// Initial restore attempt (in case the load event already fired)
await restoreScrollPosition();
}
// --- Feature: Catalog & Image Hover ---
async function featureImageHover() {
// Accepts the thumb
node as the first argument
function getFullMediaSrcFromMime(thumbNode, filemime) {
if (!thumbNode || !filemime) return null;
const thumbnailSrc = thumbNode.getAttribute("src");
// If it's a t_ thumbnail, replace as before
if (/\/t_/.test(thumbnailSrc)) {
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",
"image/bmp": ".bmp",
"video/mp4": ".mp4",
"video/webm": ".webm",
"audio/ogg": ".ogg",
"audio/mpeg": ".mp3",
"audio/x-m4a": ".m4a",
"audio/wav": ".wav",
};
const ext = mimeToExt[filemime.toLowerCase()];
if (!ext) return null;
return base + ext;
}
// If it's a /spoiler.png thumbnail or /custom.spoiler, use parent 's href
if (
/\/spoiler\.png$/i.test(thumbnailSrc) ||
/\/custom\.spoiler$/i.test(thumbnailSrc) ||
/\/audioGenericThumb\.png$/i.test(thumbnailSrc)
) {
const parentA = thumbNode.closest("a.linkThumb, a.imgLink");
if (parentA && parentA.getAttribute("href")) {
// Use the full file URL from href
return parentA.getAttribute("href");
}
return null;
}
// Fallback: return null if not recognized
return null;
}
// Inject CSS for the audio indicator (only once)
if (!document.getElementById("audio-preview-indicator-style")) {
const style = document.createElement("style");
style.id = "audio-preview-indicator-style";
style.textContent = `
/* Make containers position:relative so absolute positioning works */
a.imgLink[data-filemime^="audio/"],
a.originalNameLink[href$=".mp3"],
a.originalNameLink[href$=".ogg"],
a.originalNameLink[href$=".m4a"],
a.originalNameLink[href$=".wav"] {
position: relative;
}
.audio-preview-indicator {
display: none;
position: absolute;
background: rgba(0, 0, 0, 0.7);
color: #ffffff;
padding: 5px;
font-size: 12px;
border-radius: 3px;
z-index: 1000;
left: 0;
top: 0;
white-space: nowrap;
pointer-events: none;
}
a[data-filemime^="audio/"]:hover .audio-preview-indicator,
a.originalNameLink:hover .audio-preview-indicator {
display: block;
}
`;
document.head.appendChild(style);
}
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" ||
floatingMedia.tagName === "AUDIO"
) {
try {
floatingMedia.pause();
floatingMedia.removeAttribute("src");
floatingMedia.load();
} catch (e) {
// Silently handle media cleanup errors
}
}
if (floatingMedia.parentNode) {
floatingMedia.parentNode.removeChild(floatingMedia);
}
}
// Remove any audio indicators
const indicators = document.querySelectorAll(".audio-preview-indicator");
indicators.forEach((indicator) => {
if (indicator.parentNode) {
indicator.parentNode.removeChild(indicator);
}
});
floatingMedia = null;
lastThumb = null;
isStillHovering = false;
document.removeEventListener("mousemove", onMouseMove);
}
function onMouseMove(event) {
if (!floatingMedia) return;
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
// Determine media dimensions based on type
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;
} else if (floatingMedia.tagName === "AUDIO") {
// Don't move audio elements - they're hidden anyway
return;
}
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";
}
async function onThumbEnter(e) {
const thumb = e.currentTarget;
if (lastThumb === thumb) return;
lastThumb = thumb;
cleanupFloatingMedia();
isStillHovering = true;
// Get the actual container element (important for audio files)
const container =
thumb.tagName === "IMG"
? thumb.closest("a.linkThumb, a.imgLink")
: thumb;
function onLeave() {
isStillHovering = false;
cleanupFloatingMedia();
}
thumb.addEventListener("mouseleave", onLeave, { once: true });
hoverTimeout = setTimeout(async () => {
hoverTimeout = null;
if (!isStillHovering) return;
let filemime = null;
let fullSrc = null;
// Case 1: Image/video thumbnail
if (thumb.tagName === "IMG") {
const parentA = thumb.closest("a.linkThumb, a.imgLink");
if (!parentA) return;
const href = parentA.getAttribute("href");
if (!href) return;
const ext = href.split(".").pop().toLowerCase();
filemime =
parentA.getAttribute("data-filemime") ||
{
jpg: "image/jpeg",
jpeg: "image/jpeg",
png: "image/png",
gif: "image/gif",
webp: "image/webp",
bmp: "image/bmp",
mp4: "video/mp4",
webm: "video/webm",
ogg: "audio/ogg",
mp3: "audio/mpeg",
m4a: "audio/x-m4a",
wav: "audio/wav",
}[ext];
fullSrc = getFullMediaSrcFromMime(thumb, filemime);
}
// Case 2: Audio file download link
else if (thumb.classList.contains("originalNameLink")) {
const href = thumb.getAttribute("href");
if (!href) return;
const ext = href.split(".").pop().toLowerCase();
if (["mp3", "ogg", "m4a", "wav"].includes(ext)) {
filemime = {
ogg: "audio/ogg",
mp3: "audio/mpeg",
m4a: "audio/x-m4a",
wav: "audio/wav",
}[ext];
fullSrc = href;
}
}
if (!fullSrc || !filemime) return;
let loaded = false;
// Helper to set common styles for floating media
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);
// Handle different media types
if (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.startsWith("video/")) {
floatingMedia = document.createElement("video");
setCommonStyles(floatingMedia);
floatingMedia.autoplay = true;
floatingMedia.loop = true;
floatingMedia.muted = false;
floatingMedia.playsInline = true;
floatingMedia.controls = false; // No controls for videos
// Set volume from settings (0-100)
let volume = 50;
if (typeof getSetting === "function") {
try {
volume = await getSetting("hoverVideoVolume");
} catch (e) {
// Use default if setting can't be retrieved
}
}
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;
} else if (filemime.startsWith("audio/")) {
// --- AUDIO HOVER INDICATOR LOGIC ---
// Remove any lingering indicator first
const oldIndicator = container.querySelector(
".audio-preview-indicator"
);
if (oldIndicator) oldIndicator.remove();
// Make sure container has position:relative for proper indicator positioning
if (container && !container.style.position) {
container.style.position = "relative";
}
floatingMedia = document.createElement("audio");
floatingMedia.src = fullSrc;
floatingMedia.volume = 0.5;
floatingMedia.controls = false; // No controls for audio
floatingMedia.style.display = "none"; // Hide the element visually
document.body.appendChild(floatingMedia);
// Add indicator to the container (parent a tag) instead of the img
const indicator = document.createElement("div");
indicator.classList.add("audio-preview-indicator");
indicator.textContent = "▶ Playing audio...";
container.appendChild(indicator);
floatingMedia.play().catch((error) => {
console.error("Audio playback failed:", error);
});
// Remove audio and indicator on click as well
function removeAudioAndIndicator() {
if (floatingMedia) {
floatingMedia.pause();
floatingMedia.currentTime = 0;
floatingMedia.remove();
floatingMedia = null;
}
if (indicator) {
indicator.remove();
}
}
container.addEventListener("click", removeAudioAndIndicator, {
once: true,
});
}
}, 120); // Short delay before showing preview
}
function attachThumbListeners(root = document) {
// Attach to image thumbnails (works for both thread and catalog)
const thumbs = root.querySelectorAll(
"a.linkThumb > img, a.imgLink > img"
);
thumbs.forEach((thumb) => {
if (!thumb._fullImgHoverBound) {
thumb.addEventListener("mouseenter", onThumbEnter);
thumb._fullImgHoverBound = true;
}
});
// Always attach to audio download links (both catalog and thread)
const audioLinks = root.querySelectorAll("a.originalNameLink");
audioLinks.forEach((link) => {
const href = link.getAttribute("href") || "";
const ext = href.split(".").pop().toLowerCase();
if (
["mp3", "wav", "ogg", "m4a"].includes(ext) &&
!link._audioHoverBound
) {
link.addEventListener("mouseenter", onThumbEnter);
link._audioHoverBound = true;
}
});
}
// Initial attachment
attachThumbListeners();
// Watch for new elements
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node.nodeType === Node.ELEMENT_NODE) {
attachThumbListeners(node);
}
}
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
// --- Feature: Save Name Checkbox ---
// Pay attention that it needs to work on localStorage for the name key (not GM Storage)
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(async (link) => {
const img = link.querySelector("img");
if (img) {
// Check if this is a custom spoiler image
const isCustomSpoiler = img.src.includes("/a/custom.spoiler");
// Check if this is NOT already a thumbnail
const isNotThumbnail = !img.src.includes("/.media/t_");
if (isNotThumbnail || isCustomSpoiler) {
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 (await 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 Initialization based on Settings ---
// Because getSetting is now async, we need to await settings before running features.
// We'll use an async IIFE for initialization:
(async function initFeatures() {
// Always run hide/show feature (it will respect settings)
await featureCssClassToggles();
if (await getSetting("blurSpoilers")) {
featureBlurSpoilers();
}
if (await getSetting("enableHeaderCatalogLinks")) {
featureHeaderCatalogLinks();
}
if (await getSetting("enableScrollSave")) {
featureSaveScrollPosition();
}
if (await getSetting("enableSaveName")) {
featureSaveNameCheckbox();
}
if (await getSetting("enableScrollArrows")) {
featureScrollArrows();
}
if ((await getSetting("beepOnYou")) || (await 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 && (await getSetting("enableCatalogImageHover"))) ||
(!isCatalogPage && (await getSetting("enableThreadImageHover")))
) {
featureImageHover();
}
})();
// --- Feature: CSS Class Toggles ---
async function featureCssClassToggles() {
document.documentElement.classList.add("8chanSS");
const classToggles = {
enableFitReplies: "fit-replies",
enableSidebar: "ss-sidebar",
enableStickyQR: "sticky-qr",
enableBottomHeader: "bottom-header",
hideBanner: "disable-banner",
hidePostingForm: "hide-posting-form",
hideAnnouncement: "hide-announcement",
hidePanelMessage: "hide-panelmessage",
alwaysShowTW: "sticky-tw",
hidePostingForm_showCatalogForm: "show-catalog-form",
};
for (const [settingKey, className] of Object.entries(classToggles)) {
if (await getSetting(settingKey)) {
document.documentElement.classList.add(className);
} else {
document.documentElement.classList.remove(className);
}
}
// URL-based class toggling
const urlClassMap = [
{ pattern: /\/catalog\.html$/i, className: "is-catalog" },
{ pattern: /\/res\/[^/]+\.html$/i, className: "is-thread" },
{ pattern: /^\/$/, className: "is-index" },
];
const currentPath = window.location.pathname.toLowerCase();
urlClassMap.forEach(({ pattern, className }) => {
if (pattern.test(currentPath)) {
document.documentElement.classList.add(className);
} else {
document.documentElement.classList.remove(className);
}
});
}
// Init
featureCssClassToggles();
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// ---- Feature: Thread Watcher Things ---
// Move new post notification
function moveWatchedNotification() {
document.querySelectorAll(".watchedCellLabel").forEach((label) => {
const notif = label.querySelector(".watchedNotification");
const link = label.querySelector("a");
if (notif && link && notif.nextSibling !== link) {
label.insertBefore(notif, link);
}
});
}
// Initial run
moveWatchedNotification();
// Observe for dynamic changes in the watched menu
const watchedMenu = document.getElementById("watchedMenu");
if (watchedMenu) {
const observer = new MutationObserver(() => moveWatchedNotification());
observer.observe(watchedMenu, { childList: true, subtree: true });
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// --- Keyboard Shortcuts ---
// Open 8chanSS menu (CTRL + F1)
document.addEventListener("keydown", async function (event) {
if (event.ctrlKey && event.key === "F1") {
event.preventDefault(); // Prevent browser help
let menu =
document.getElementById("8chanSS-menu") || (await createSettingsMenu());
menu.style.display =
menu.style.display === "none" || menu.style.display === ""
? "block"
: "none";
}
});
// Submit post (CTRL + Enter)
function submitWithCtrlEnter(event) {
// Check if Ctrl + Enter is pressed
if (event.ctrlKey && event.key === "Enter") {
event.preventDefault(); // Prevent default behavior
// Find and click the submit button
const submitButton = document.getElementById("qrbutton");
if (submitButton) {
submitButton.click();
}
}
}
// Add the event listener to the reply textarea
const replyTextarea = document.getElementById("qrbody");
if (replyTextarea) {
replyTextarea.addEventListener("keydown", submitWithCtrlEnter);
}
// 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
if (/^8chan\.(se|moe)$/.test(currentHost)) {
// General CSS for all pages
const css = `
/* Margins */
:not(.is-catalog) body {
margin: 0;
}
:root.ss-sidebar #mainPanel {
margin-right: 305px;
}
/* Cleanup */
:root.hide-posting-form #postingForm,
:root.hide-announcement #dynamicAnnouncement,
:root.hide-panelmessage #panelMessage,
#navFadeEnd,
#navFadeMid,
#navTopBoardsSpan {
display: none;
}
:root.is-catalog.show-catalog-form #postingForm {
display: block !important;
}
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 */
:root.sticky-tw #watchedMenu {
display: flex !important;
}
#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;
}
.watchedNotification::before {
padding-right: 2px;
}
/* Posts */
:root.ss-sidebar .quoteTooltip {
/* Prevent quotes from overlapping the sidebar */
max-width: calc(100vw - 305px - 24px);
right: 322px;
word-wrap: anywhere;
}
.quoteTooltip .innerPost {
overflow: hidden;
box-shadow: -3px 3px 2px 0px rgba(0,0,0,0.19);
}
:root.fit-replies :not(.hidden).innerPost {
margin-left: 10px;
display: flow-root;
}
:root.fit-replies .quoteTooltip {
display: table !important;
}
.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);
}
// 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.sticky-qr #qrbody {
resize: vertical;
max-height: 50vh;
height: 130px;
}
#qrbody {
min-width: 300px;
}
: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;
}
.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);
}
// Catalog page CSS
if (/\/catalog\.html$/.test(currentPath)) {
const css = `
#dynamicAnnouncement {
display: none;
}
#postingForm {
margin: 2em auto;
}
`;
addCustomCSS(css);
}
})();