Declutter Pinterest

Removes intrusive Pinterest shopping promotions, ads, and clutter, and makes the website more user-friendly

// ==UserScript==
// @name         Declutter Pinterest
// @namespace    August4067
// @version      1.0.0-alpha
// @description  Removes intrusive Pinterest shopping promotions, ads, and clutter, and makes the website more user-friendly
// @license      MIT
// @match        https://www.pinterest.com/*
// @match        https://*.pinterest.com/*
// @match        https://*.pinterest.co.uk/*
// @match        https://*.pinterest.fr/*
// @match        https://*.pinterest.de/*
// @match        https://*.pinterest.ca/*
// @match        https://*.pinterest.jp/*
// @match        https://*.pinterest.it/*
// @match        https://*.pinterest.au/*
// @icon         https://www.pinterest.com/favicon.ico
// @require      https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @sandbox      Javascript
// ==/UserScript==

/*--- waitForKeyElements():  A utility function, for Greasemonkey scripts,
    that detects and handles AJAXed content.

    Usage example:

        waitForKeyElements (
            "div.comments"
            , commentCallbackFunction
        );

        //--- Page-specific function to do what we want when the node is found.
        function commentCallbackFunction (jNode) {
            jNode.text ("This comment changed by waitForKeyElements().");
        }

    IMPORTANT: This function requires your script to have loaded jQuery.
*/

// Pulled from: https://gist.github.com/raw/2625891/waitForKeyElements.js
function waitForKeyElements(
  selectorTxt /* Required: The jQuery selector string that
                        specifies the desired element(s).
                    */,
  actionFunction /* Required: The code to run when elements are
                        found. It is passed a jNode to the matched
                        element.
                    */,
  bWaitOnce /* Optional: If false, will continue to scan for
                        new elements even after the first match is
                        found.
                    */,
  iframeSelector /* Optional: If set, identifies the iframe to
                        search.
                    */,
) {
  var targetNodes, btargetsFound;

  if (typeof iframeSelector == "undefined") targetNodes = $(selectorTxt);
  else targetNodes = $(iframeSelector).contents().find(selectorTxt);

  if (targetNodes && targetNodes.length > 0) {
    btargetsFound = true;
    /*--- Found target node(s).  Go through each and act if they
            are new.
        */
    targetNodes.each(function () {
      var jThis = $(this);
      var alreadyFound = jThis.data("alreadyFound") || false;

      if (!alreadyFound) {
        //--- Call the payload function.
        var cancelFound = actionFunction(jThis);
        if (cancelFound) btargetsFound = false;
        else jThis.data("alreadyFound", true);
      }
    });
  } else {
    btargetsFound = false;
  }

  //--- Get the timer-control variable for this selector.
  var controlObj = waitForKeyElements.controlObj || {};
  var controlKey = selectorTxt.replace(/[^\w]/g, "_");
  var timeControl = controlObj[controlKey];

  //--- Now set or clear the timer as appropriate.
  if (btargetsFound && bWaitOnce && timeControl) {
    //--- The only condition where we need to clear the timer.
    clearInterval(timeControl);
    delete controlObj[controlKey];
  } else {
    //--- Set a timer, if needed.
    if (!timeControl) {
      timeControl = setInterval(function () {
        waitForKeyElements(
          selectorTxt,
          actionFunction,
          bWaitOnce,
          iframeSelector,
        );
      }, 300);
      controlObj[controlKey] = timeControl;
    }
  }
  waitForKeyElements.controlObj = controlObj;
}

// We will set the Pinterest page title to this, to remove
// the flashing title notifications like Pinterest (2)
const ORIGINAL_TITLE = "Pinterest";

const SETTINGS_CONFIG = {
  disableVideoAutoplay: {
    displayName: "Disable video autoplay",
    default: true,
  },
  removeShoppablePins: {
    displayName: "Remove shoppable pins",
    default: true,
  },
};

class Setting {
  constructor(name, config) {
    this.name = name;
    this.displayName = config.displayName;
    this.default = config.default;
  }

  currentValue() {
    return GM_getValue(this.name, this.default);
  }

  toggleSetting() {
    GM_setValue(this.name, !this.currentValue());
  }
}

// Create settings object by mapping config to Setting instances
const SETTINGS = Object.fromEntries(
  Object.entries(SETTINGS_CONFIG).map(([name, config]) => [
    name,
    new Setting(name, config),
  ]),
);

// MENU SETTINGS
function toggleMenuSetting(settingName) {
  var setting = SETTINGS[settingName];
  setting.toggleSetting();
  updateSettingsMenu();
  console.debug(`Setting ${settingName} set to: ${setting.currentValue()}}`);
  location.reload();
}

function updateSettingsMenu() {
  for (const [setting_name, setting] of Object.entries(SETTINGS)) {
    GM_registerMenuCommand(
      `${setting.displayName}: ${setting.currentValue() ? "Enabled" : "Disabled"}`,
      () => {
        toggleMenuSetting(setting_name);
      },
    );
  }
}

// HELPER FUNCTIONS
function waitAndRemove(selector, removeFunction) {
  if (removeFunction == undefined) {
    removeFunction = (elem) => hideElement(elem);
  }

  waitForKeyElements(selector, function (node) {
    if (node && node.length > 0) {
      removeFunction(node[0]);
    }
  });
}

/**
 * Hide an element by setting its display style to "none" if it exists
 * @param {HTMLElement} element - The DOM element to hide
 */
function hideElement(element) {
  if (element) {
    element.style.setProperty("display", "none", "important");
  }
}

function isFeaturedBoard(pin) {
  if (
    pin.textContent.trim().toLowerCase().startsWith("explore featured boards")
  ) {
    return true;
  }
  return false;
}

function isShoppingCard(pin) {
  if (
    pin
      .querySelector("h2#comments-heading")
      ?.textContent.toLowerCase()
      .startsWith("shop")
  ) {
    return true;
  } else if (
    pin
      .querySelector("a")
      ?.getAttribute("aria-label")
      ?.toLowerCase()
      .startsWith("shop")
  ) {
    return true;
  } else if (
    pin.querySelector("h2")?.textContent.trim().toLowerCase().startsWith("shop")
  ) {
    return true;
  } else if (pin.textContent.trim().toLowerCase().startsWith("shop similar")) {
    return true;
  }
  return false;
}

function isShoppablePin(pin) {
  if (SETTINGS.removeShoppablePins.currentValue()) {
    return pin.querySelector('[aria-label="Shoppable Pin indicator"]') != null;
  }
  return false;
}

function isSponsoredPin(pin) {
  return pin.querySelector('div[title="Sponsored"]') != null;
}

// FUNCTIONS THAT REMOVE
function removeClutterPins(pins) {
  const filters = [
    isShoppingCard,
    isShoppablePin,
    isFeaturedBoard,
    isSponsoredPin,
  ];

  pins.forEach((pin) => {
    if (filters.some((test) => test(pin))) {
      hideElement(pin);
    }
  });
}

// In the #SuggestionsMenu
function removePopularOnPinterestSearchSuggestions() {
  waitAndRemove('div[data-test-id="search-story-suggestions-container"]');
}

function setupSearchSuggestionsRemovalForPopularSuggestions() {
  waitAndRemove("#searchBoxContainer", (node) => {
    const observer = new MutationObserver(() => {
      removePopularOnPinterestSearchSuggestions();
    });
    observer.observe(node, { childList: true, subtree: true });
  });
}

// FUNCTION THAT SETUP OBSERVERS
function setupPinFiltering() {
  waitAndRemove('div[role="list"]', (node) => {
    var pinListMutationObserver = new MutationObserver(
      (mutations, observer) => {
        removeClutterPins(node.querySelectorAll('div[role="listitem"]'));
      },
    );
    pinListMutationObserver.observe(node, {
      childList: true,
      subtree: true,
    });
  });
}

function setupShopButtonRemovalFromBoardTools() {
  waitAndRemove('div[data-test-id="board-tools"] div[data-test-id="Shop"]');
}

function removeExploreTabNotificationsIcon() {
  // --- Remove notification icon from Explore tab in the top nav (old behavior)
  var exploreTab = document.querySelector('div[data-test-id="today-tab"]');
  if (exploreTab) {
    var notificationsIcon = exploreTab.querySelector(
      'div[aria-label="Notifications"]',
    );
    hideElement(notificationsIcon);
  }

  // --- Remove notification badge from Explore tab in the sidebar (new behavior)
  // Find the Explore tab link in the sidebar
  var exploreTabLink = document.querySelector('a[data-test-id="today-tab"]');
  if (exploreTabLink) {
    // The parent of the link is the icon container, its parent is the sidebar item
    var iconContainer = exploreTabLink.closest('div[class*="XiG"]');
    var sidebarItem = iconContainer?.parentElement?.parentElement;
    if (sidebarItem) {
      // The notification badge is a sibling div with class "MIw" and pointer-events: none
      var notificationBadge = sidebarItem.parentElement?.querySelector(
        '.MIw[style*="pointer-events: none"]',
      );
      if (notificationBadge) {
        hideElement(notificationBadge);
      }
    }
  }
}

function setupSidebarObserverForExploreNotifications() {
  // Find the sidebar navigation container (adjust selector if needed)
  const sidebarNav =
    document.querySelector('nav[id="VerticalNavContent"]') ||
    document.querySelector('div[role="navigation"]');
  if (!sidebarNav) {
    // Try again later if sidebar not yet loaded
    setTimeout(setupSidebarObserverForExploreNotifications, 500);
    return;
  }
  // Remove any existing badge immediately
  removeExploreTabNotificationsIcon();

  // Set up observer
  const observer = new MutationObserver(() => {
    removeExploreTabNotificationsIcon();
  });
  observer.observe(sidebarNav, { childList: true, subtree: true });
}

function removeShopByBanners() {
  waitForKeyElements('div[data-test-id="sf-header-heading"]', function (node) {
    var shopByBannerAtTopOfBoard = node[0].closest(
      'div[class="PKX zI7 iyn Hsu"]',
    );
    hideElement(shopByBannerAtTopOfBoard);

    if (node[0].closest('div[data-test-id="base-board-pin-grid"]')) {
      var shopByBannerAtBottomOfBoard = node[0].closest(
        'div[class="gcw zI7 iyn Hsu"]',
      );
      hideElement(shopByBannerAtBottomOfBoard);
    }

    var shopByBannerAtTopOfSearch = node[0].closest('div[role="listitem"]');
    hideElement(shopByBannerAtTopOfSearch);
  });
}

function disableVideoAutoplay() {
  waitForKeyElements(
    "div[role='list'] video",
    function (videoNode) {
      if (videoNode && videoNode.length > 0) {
        videoNode.prop("autoplay", false);
        videoNode.prop("muted", false);
        videoNode.prop("loop", false);
        videoNode.prop("playsinline", false);
        videoNode.prop("preload", "metadata");
        videoNode[0].pause();
        console.debug("Stopped video autoplay");
      }
    },
    false,
  );
}

function setupDisablingVideoAutoplay() {
  waitForKeyElements(
    "div[role='list']",
    function (listNode) {
      const observer = new MutationObserver((mutationsList) => {
        for (const mutation of mutationsList) {
          if (mutation.addedNodes.length > 0) {
            disableVideoAutoplay();
          }
        }
      });

      observer.observe(listNode[0], { childList: true, subtree: true });

      disableVideoAutoplay();
    },
    false,
  );
}

function main() {
  "use strict";

  updateSettingsMenu();

  setupPinFiltering();
  setupShopButtonRemovalFromBoardTools();
  setupSidebarObserverForExploreNotifications();
  setupSearchSuggestionsRemovalForPopularSuggestions();

  removeShopByBanners();
  disableVideoAutoplay();
}

main();

let lastUrl = window.location.href;
setInterval(() => {
  const currentUrl = window.location.href;
  if (currentUrl !== lastUrl) {
    console.debug(
      `Detected new page, currentURL=${currentUrl}, previousURL=${lastUrl}`,
    );
    lastUrl = currentUrl;
    main();
  }
}, 750);

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址