Nexus Mods Mod Blacklister

Blacklist mods by title on Nexus Mods

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Nexus Mods Mod Blacklister
// @namespace    https://www.nexusmods.com/
// @version      3
// @license      MIT
// @description  Blacklist mods by title on Nexus Mods
// @author       PhiZero
// @include      http://www.nexusmods.com/*
// @include      https://www.nexusmods.com/*
// @run-at       document-idle
// @grant        none
// ==/UserScript==

(function () {
  "use strict";

  // Styles
  const styles = {
    button:
      "margin-bottom: 1em; width: 100%; background: #222; color: #fff; border: 1px solid #444; border-radius: 4px; padding: 0.5em; cursor: pointer; font-size: 14px; transition: background-color 0.2s, border-color 0.2s;",
    popupOverlay:
      "position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.7); z-index: 9999; display: flex; align-items: center; justify-content: center;",
    popupDialog:
      "background: #2a2a2a; color: #ffffff; padding: 2em; border-radius: 8px; min-width: 400px; max-width: 90vw; max-height: 80vh; box-shadow: 0 4px 20px rgba(0,0,0,0.5); overflow-y: auto;",
    popupTitle: "margin-top: 0; margin-bottom: 1em; color: #ffffff;",
    popupDescription: "margin-bottom: 1.5em; color: #cccccc;",
    entriesContainer: "margin-bottom: 1.5em;",
    entryRow:
      "display: flex; gap: 0.5em; margin-bottom: 0.5em; align-items: center;",
    entryInput:
      "flex: 1; padding: 0.5em; border: 1px solid #555; border-radius: 4px; background: #1a1a1a; color: #ffffff; font-size: 14px;",
    deleteButton:
      "width: 30px; height: 30px; border: none; border-radius: 4px; background: #d32f2f; color: white; cursor: pointer; font-size: 18px; display: flex; align-items: center; justify-content: center;",
    addButton:
      "background: #4caf50; color: white; border: none; border-radius: 4px; padding: 0.5em 1em; cursor: pointer; margin-bottom: 1.5em; font-size: 14px;",
    buttonContainer: "display: flex; gap: 1em; justify-content: flex-end;",
    saveButton:
      "background: #4caf50; color: white; border: none; border-radius: 4px; padding: 0.5em 1.5em; cursor: pointer; font-size: 14px;",
    cancelButton:
      "background: #f44336; color: white; border: none; border-radius: 4px; padding: 0.5em 1.5em; cursor: pointer; font-size: 14px;",
  };

  const STORAGE_KEY = "nexusmods_blacklist";

  function loadBlacklist() {
    const stored = localStorage.getItem(STORAGE_KEY);
    if (stored) {
      try {
        return JSON.parse(stored);
      } catch (e) {
        console.warn(
          "[NexusMods Blacklister] Failed to parse blacklist from storage"
        );
      }
    }
    return [""];
  }

  function saveBlacklist(list) {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(list));
  }

  let blacklist = loadBlacklist();

  function isBlacklisted(title) {
    return blacklist.some((term) =>
      title.toLowerCase().includes(term.toLowerCase())
    );
  }

function updateBlockedCount(blockedCount) {
    const resultCountElem = document.querySelector(
      '[data-e2eid="result-count"]'
    );
    if (!resultCountElem) return;

    const match = resultCountElem.textContent.match(/^(\d+(?:,\d+)*) results/i);
    if (!match) return;

    const baseText = `${match[1]} results`;

    if (blockedCount > 0) {
      resultCountElem.textContent = `${baseText} (${blockedCount} blocked)`;
    } else {
      resultCountElem.textContent = baseText;
    }
  }

  let hasInitialized = false;
  let gridObserver = null;
  let currentUrl = window.location.href;
  let isTransitioning = false;

  function setupNavigationInterception() {
    const originalPushState = history.pushState;
    const originalReplaceState = history.replaceState;

    history.pushState = function (...args) {
      isTransitioning = true;
      cleanup();
      const result = originalPushState.apply(history, args);

      setTimeout(() => {
        isTransitioning = false;
        currentUrl = window.location.href;
        initializeImmediately();
      }, 500);

      return result;
    };

    history.replaceState = function (...args) {
      isTransitioning = true;
      cleanup();
      const result = originalReplaceState.apply(history, args);

      setTimeout(() => {
        isTransitioning = false;
        currentUrl = window.location.href;
        initializeImmediately();
      }, 500);

      return result;
    };

    window.addEventListener("popstate", () => {
      isTransitioning = true;
      cleanup();

      setTimeout(() => {
        isTransitioning = false;
        currentUrl = window.location.href;
        initializeImmediately();
      }, 500);
    });
  }

  function removeBlacklistedMods() {
    if (isTransitioning) return;

    const grid = document.querySelector(".mods-grid");
    if (!grid) return;

    const modTiles = grid.querySelectorAll('[data-e2eid="mod-tile"]');
    let blocked = 0;

    modTiles.forEach((tile) => {
      try {
        if (isTransitioning) return;

        const titleLink = tile.querySelector('[data-e2eid="mod-tile-title"]');
        if (titleLink && isBlacklisted(titleLink.textContent.trim())) {
          if (tile.style.display !== "none") {
            tile.style.display = "none";
            blocked++;
          }
        } else {
          if (tile.style.display === "none") {
            tile.style.display = "";
          }
        }
      } catch (e) {
        // Silently ignore DOM manipulation errors during page transitions
      }
    });

    updateBlockedCount(blocked);
  }

  function checkAndInitialize() {
    if (hasInitialized || isTransitioning) return;

    hasInitialized = true;

    addBlacklistButton();
    removeBlacklistedMods();

    const grid = document.querySelector(".mods-grid");
    if (grid) {
      gridObserver = new MutationObserver((mutations) => {
        if (isTransitioning) return;

        let hasNewMods = false;

        for (const mutation of mutations) {
          if (isTransitioning) return;

          for (const node of mutation.addedNodes) {
            if (
              node.nodeType === Node.ELEMENT_NODE &&
              node.matches('[data-e2eid="mod-tile"]')
            ) {
              hasNewMods = true;
              break;
            }
          }
          if (hasNewMods) break;
        }

        if (hasNewMods && !isTransitioning) {
          setTimeout(() => {
            if (!isTransitioning) {
              removeBlacklistedMods();
            }
          }, 100);
        }
      });

      gridObserver.observe(grid, { childList: true });
    }
  }

  function cleanup() {
    if (gridObserver) {
      gridObserver.disconnect();
      gridObserver = null;
    }
    hasInitialized = false;
  }

  function initializeImmediately() {
    if (hasInitialized || isTransitioning) return;
    checkAndInitialize();
  }

  setupNavigationInterception();

  window.addEventListener("beforeunload", cleanup);
  window.addEventListener("pagehide", cleanup);

  if (document.readyState === "loading") {
    window.addEventListener("DOMContentLoaded", initializeImmediately);
  } else {
    initializeImmediately();
  }

  window.addEventListener("load", () => {
    if (!hasInitialized && !isTransitioning) {
      setTimeout(() => {
        if (!isTransitioning) {
          checkAndInitialize();
        }
      }, 200);
    }
  });

  function addBlacklistButton() {
    if (document.querySelector("#edit-blacklist-btn")) {
      return true;
    }

    let filtersPanel = document.querySelector('[aria-label="Filters panel"]');
    if (!filtersPanel) {
      filtersPanel = document.querySelector("#filters-panel");
      if (!filtersPanel) return false;
    }

    const btn = document.createElement("button");
    btn.id = "edit-blacklist-btn";
    btn.textContent = "Edit Mod Blacklist";
    btn.style = styles.button;
    btn.onmouseover = () => {
      btn.style.backgroundColor = "#333";
      btn.style.borderColor = "#555";
    };
    btn.onmouseout = () => {
      btn.style.backgroundColor = "#222";
      btn.style.borderColor = "#444";
    };
    btn.onclick = showBlacklistPopup;

    const hideBtn = filtersPanel.querySelector(
      '[data-e2eid="hide-filters-panel"]'
    );

    if (hideBtn) {
      const container = hideBtn.closest(".mb-6") || hideBtn.parentElement;
      if (container && container.parentElement) {
        const nextSibling = container.nextSibling;
        if (nextSibling) {
          container.parentElement.insertBefore(btn, nextSibling);
        } else {
          container.parentElement.appendChild(btn);
        }
        return true;
      }
    }

    filtersPanel.insertBefore(btn, filtersPanel.firstChild);
    return true;
  }

  function showBlacklistPopup() {
    if (document.getElementById("blacklist-popup")) return;

    const popup = document.createElement("div");
    popup.id = "blacklist-popup";
    popup.style = styles.popupOverlay;

    const dialog = document.createElement("div");
    dialog.style = styles.popupDialog;

    const title = document.createElement("h2");
    title.textContent = "Edit Mod Blacklist";
    title.style = styles.popupTitle;

    const description = document.createElement("p");
    description.textContent =
      "Mods containing any of these terms will be hidden from the grid.";
    description.style = styles.popupDescription;

    const entriesContainer = document.createElement("div");
    entriesContainer.id = "blacklist-entries";
    entriesContainer.style = styles.entriesContainer;

    function createEntryRow(text = "", isNew = false) {
      const row = document.createElement("div");
      row.style = styles.entryRow;

      const input = document.createElement("input");
      input.type = "text";
      input.value = text;
      input.placeholder = "Enter blacklist term...";
      input.style = styles.entryInput;

      const deleteBtn = document.createElement("button");
      deleteBtn.textContent = "−";
      deleteBtn.style = styles.deleteButton;
      deleteBtn.onclick = () => row.remove();

      row.appendChild(input);
      row.appendChild(deleteBtn);

      if (isNew) {
        input.focus();
      }

      return row;
    }

    function addNewEntry() {
      const newRow = createEntryRow("", true);
      entriesContainer.appendChild(newRow);
    }

    blacklist.forEach((term) => {
      entriesContainer.appendChild(createEntryRow(term));
    });

    if (blacklist.length === 0) {
      entriesContainer.appendChild(createEntryRow("", true));
    }

    const addBtn = document.createElement("button");
    addBtn.textContent = "+ Add Entry";
    addBtn.style = styles.addButton;
    addBtn.onclick = addNewEntry;

    const buttonsDiv = document.createElement("div");
    buttonsDiv.style = styles.buttonContainer;

    const saveBtn = document.createElement("button");
    saveBtn.textContent = "Save";
    saveBtn.style = styles.saveButton;

    const cancelBtn = document.createElement("button");
    cancelBtn.textContent = "Cancel";
    cancelBtn.style = styles.cancelButton;

    saveBtn.onclick = () => {
      const inputs = entriesContainer.querySelectorAll("input");
      blacklist = Array.from(inputs)
        .map((input) => input.value.trim())
        .filter((val) => val.length > 0);

      saveBlacklist(blacklist);
      popup.remove();
      removeBlacklistedMods();
    };

    cancelBtn.onclick = () => popup.remove();

    popup.onclick = (e) => {
      if (e.target === popup) popup.remove();
    };

    buttonsDiv.appendChild(saveBtn);
    buttonsDiv.appendChild(cancelBtn);

    dialog.appendChild(title);
    dialog.appendChild(description);
    dialog.appendChild(entriesContainer);
    dialog.appendChild(addBtn);
    dialog.appendChild(buttonsDiv);

    popup.appendChild(dialog);
    document.body.appendChild(popup);
  }
})();