Greasy Fork

Greasy Fork is available in English.

8chan Style Script

Script to style 8chan

当前为 2025-04-21 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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.23
// @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 },
      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);
  }

  // --- 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 = "350px";
    menu.style.fontFamily = "sans-serif";
    menu.style.userSelect = "none";

    // Draggable
    let isDragging = false,
      dragOffsetX = 0,
      dragOffsetY = 0;
    const header = document.createElement("div");
    header.style.display = "flex";
    header.style.justifyContent = "space-between";
    header.style.alignItems = "center";
    header.style.marginBottom = "0";
    header.style.cursor = "move";
    header.style.background = "#333";
    header.style.padding = "5px 18px 5px";
    header.style.borderTopLeftRadius = "8px";
    header.style.borderTopRightRadius = "8px";
    header.addEventListener("mousedown", function (e) {
      isDragging = true;
      const rect = menu.getBoundingClientRect();
      dragOffsetX = e.clientX - rect.left;
      dragOffsetY = e.clientY - rect.top;
      document.body.style.userSelect = "none";
    });
    document.addEventListener("mousemove", function (e) {
      if (!isDragging) return;
      let newLeft = e.clientX - dragOffsetX;
      let newTop = e.clientY - dragOffsetY;
      const menuRect = menu.getBoundingClientRect();
      const menuWidth = menuRect.width;
      const menuHeight = menuRect.height;
      const viewportWidth = window.innerWidth;
      const viewportHeight = window.innerHeight;
      newLeft = Math.max(0, Math.min(newLeft, viewportWidth - menuWidth));
      newTop = Math.max(0, Math.min(newTop, viewportHeight - menuHeight));
      menu.style.left = newLeft + "px";
      menu.style.top = newTop + "px";
      menu.style.right = "auto";
    });
    document.addEventListener("mouseup", function () {
      isDragging = false;
      document.body.style.userSelect = "";
    });

    // Title and close button
    const title = document.createElement("span");
    title.textContent = "8chanSS Settings";
    title.style.fontWeight = "bold";
    header.appendChild(title);

    const closeBtn = document.createElement("button");
    closeBtn.textContent = "✕";
    closeBtn.style.background = "none";
    closeBtn.style.border = "none";
    closeBtn.style.color = "#fff";
    closeBtn.style.fontSize = "18px";
    closeBtn.style.cursor = "pointer";
    closeBtn.style.marginLeft = "10px";
    closeBtn.addEventListener("click", () => {
      menu.style.display = "none";
    });
    header.appendChild(closeBtn);

    menu.appendChild(header);

    // Tab navigation
    const tabNav = document.createElement("div");
    tabNav.style.display = "flex";
    tabNav.style.borderBottom = "1px solid #444";
    tabNav.style.background = "#2a2a2a";

    // Tab content container
    const tabContent = document.createElement("div");
    tabContent.style.padding = "15px 18px";
    tabContent.style.maxHeight = "60vh";
    tabContent.style.overflowY = "auto";

    // Store current (unsaved) values
    const tempSettings = {};
    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: "Styling",
        content: createTabContent("styling", tempSettings),
      },
    };

    // Create tab buttons
    Object.keys(tabs).forEach((tabId, index, arr) => {
      const tab = tabs[tabId];
      const tabButton = document.createElement("button");
      tabButton.textContent = tab.label;
      tabButton.dataset.tab = tabId;
      tabButton.style.background = index === 0 ? "#333" : "transparent";
      tabButton.style.border = "none";
      tabButton.style.borderRight = "1px solid #444";
      tabButton.style.color = "#fff";
      tabButton.style.padding = "8px 15px";
      tabButton.style.margin = "5px 0 0 0";
      tabButton.style.cursor = "pointer";
      tabButton.style.flex = "1";
      tabButton.style.fontSize = "14px";
      tabButton.style.transition = "background 0.2s";

      // Add rounded corners and margin to the first and last tab
      if (index === 0) {
        tabButton.style.borderTopLeftRadius = "8px";
        tabButton.style.margin = "5px 0 0 5px";
      }
      if (index === arr.length - 1) {
        tabButton.style.borderTopRightRadius = "8px";
        tabButton.style.margin = "5px 5px 0 0";
        tabButton.style.borderRight = "none"; // Remove border on last tab
      }

      tabButton.addEventListener("click", () => {
        // Hide all tab contents
        Object.values(tabs).forEach((t) => {
          t.content.style.display = "none";
        });

        // Show selected tab content
        tab.content.style.display = "block";

        // Update active tab button
        tabNav.querySelectorAll("button").forEach((btn) => {
          btn.style.background = "transparent";
        });
        tabButton.style.background = "#333";
      });

      tabNav.appendChild(tabButton);
    });

    menu.appendChild(tabNav);

    // Add all tab contents to the container
    Object.values(tabs).forEach((tab, index) => {
      tab.content.style.display = index === 0 ? "block" : "none";
      tabContent.appendChild(tab.content);
    });

    menu.appendChild(tabContent);

    // Button container for Save and Reset buttons
    const buttonContainer = document.createElement("div");
    buttonContainer.style.display = "flex";
    buttonContainer.style.gap = "10px";
    buttonContainer.style.padding = "0 18px 15px";

    // Save Button
    const saveBtn = document.createElement("button");
    saveBtn.textContent = "Save";
    saveBtn.style.background = "#4caf50";
    saveBtn.style.color = "#fff";
    saveBtn.style.border = "none";
    saveBtn.style.borderRadius = "4px";
    saveBtn.style.padding = "8px 18px";
    saveBtn.style.fontSize = "15px";
    saveBtn.style.cursor = "pointer";
    saveBtn.style.flex = "1";
    saveBtn.addEventListener("click", 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 = "&#9654;"; // 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",
      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";
    }
  });

  // 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 */
  #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.bottom-header #quick-reply {
      bottom: 28px !important;
  }
  #quick-reply {
      padding: 0;
      opacity: 0.7;
      transition: opacity 0.3s ease;
  }
  #quick-reply:hover,
  #quick-reply:focus-within {
      opacity: 1;
  }
  #qrbody {
      resize: vertical;
      max-height: 50vh;
      height: 130px;
  }
  .floatingMenu {
      padding: 0 !important;
  }
  #qrFilesBody {
      max-width: 300px;
  }
  /* Banner */
  :root.disable-banner #bannerImage {
      display: none;
  }
  :root.ss-sidebar #bannerImage {
      width: 305px;
      right: 0;
      position: fixed;
      top: 26px;
  }
  :root.ss-sidebar.bottom-header #bannerImage {
      top: 0 !important;
  }
  .innerUtility.top {
      margin-top: 2em;
      background-color: transparent !important;
      color: var(--link-color) !important;
  }
  .innerUtility.top a {
      color: var(--link-color) !important;
  }
  .quoteTooltip {
      z-index: 110;
  }
  /* (You) Replies */
  .innerPost:has(.youName) {
      border-left: dashed #68b723 3px;
  }
  .innerPost:has(.quoteLink.you) {
      border-left: solid #dd003e 3px;
  }
  /* Filename & Thumbs */
  .originalNameLink {
      display: inline;
      overflow-wrap: anywhere;
      white-space: normal;
  }
  .multipleUploads .uploadCell:not(.expandedCell) {
    max-width: 215px;
  }
  `;
    addCustomCSS(css);
  }

  // Catalog page CSS
  if (/\/catalog\.html$/.test(currentPath)) {
    const css = `
  #dynamicAnnouncement {
      display: none;
  }
  #postingForm {
      margin: 2em auto;
  }
  `;
    addCustomCSS(css);
  }
})();