Declutter Pinterest

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

Per 01-06-2025. Zie de nieuwste versie.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==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);