您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Script to style 8chan
当前为
- // ==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.24
- // @author OtakuDude
- // @run-at document-idle
- // @description Script to style 8chan
- // @license MIT
- // ==/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 <img> 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 /a/custom.spoiler, use parent <a>'s href
- if (
- /\/spoiler\.png$/i.test(thumbnailSrc) ||
- /\/a\/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);
- }
- })();