Declutter Pinterest

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

Verzia zo dňa 16.11.2024. Pozri najnovšiu verziu.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

// ==UserScript==
// @name         Declutter Pinterest
// @namespace    August4067
// @version      0.4.0
// @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.google.com/s2/favicons?sz=64&domain=pinterest.com
// @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";

// SETTINGS
const SETTING_REMOVE_SHOPPABLE_PINS = "removeShoppablePins";
const SETTING_REMOVE_PIN_FOOTERS = "removePinFooters";
const SETTING_REMOVE_NAVBAR_NOTIFICATION_ICON = "removeNotificationIcon";
const SETTING_REMOVE_NAVBAR_MESSAGES_ICON = "removeMessagesIcon";
const SETTING_REMOVE_COMMENTS = "removeComments";

class Setting {
  constructor(settingName, settingDisplayName, settingDefault) {
    this.settingName = settingName;
    this.settingDisplayName = settingDisplayName;
    this.settingDefault = settingDefault;
  }

  currentValue() {
    return GM_getValue(this.settingName, this.settingDefault);
  }

  toggleSetting() {
    var current = this.currentValue();
    GM_setValue(this.settingName, !current);
  }
}

class SettingRemoveShoppablePins extends Setting {
  constructor() {
    super(SETTING_REMOVE_SHOPPABLE_PINS, "Remove shoppable pins", false);
  }
}

class SettingRemovePinFooters extends Setting {
  constructor() {
    super(SETTING_REMOVE_PIN_FOOTERS, "Remove pin footers", false);
  }
}

class SettingRemoveNavbarMessagesIcon extends Setting {
  constructor() {
    super(
      SETTING_REMOVE_NAVBAR_MESSAGES_ICON,
      "Remove navbar messages icon",
      false,
    );
  }
}

class SettingRemoveNavbarNotificationsIcon extends Setting {
  constructor() {
    super(
      SETTING_REMOVE_NAVBAR_NOTIFICATION_ICON,
      "Remove navbar notifications icon",
      false,
    );
  }
}

class SettingRemoveComments extends Setting {
  constructor() {
    super(SETTING_REMOVE_COMMENTS, "Remove pin comments", false);
  }
}

const SETTINGS = {
  [SETTING_REMOVE_SHOPPABLE_PINS]: new SettingRemoveShoppablePins(),
  [SETTING_REMOVE_PIN_FOOTERS]: new SettingRemovePinFooters(),
  [SETTING_REMOVE_COMMENTS]: new SettingRemoveComments(),
  [SETTING_REMOVE_NAVBAR_MESSAGES_ICON]: new SettingRemoveNavbarMessagesIcon(),
  [SETTING_REMOVE_NAVBAR_NOTIFICATION_ICON]:
    new SettingRemoveNavbarNotificationsIcon(),
};

// 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.settingDisplayName}: ${setting.currentValue() ? "Enabled" : "Disabled"}`,
      () => {
        toggleMenuSetting(setting_name);
      },
    );
  }
}

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

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

// DECLUTTER BOARDS
/**
 * Clean the Shop button from the top of boards (from the 3 button group with "Shop", "Organize", and "More ideas")
 */
function cleanShopButtonsFromBoard() {
  console.debug("Cleaning Shop buttons from board");
  waitForKeyElements('div[data-test-id="board-tools"]', function (node) {
    var shopButton = node[0].querySelector('div[data-test-id="Shop"]');
    if (shopButton) {
      shopButton.style.display = "none";
      console.debug("Removed Shop button from top of board");
    }
  });
}

/**
 * Remove the banner of shopping pins wherever we find it (with title "Shop products inspired by this board").
 * They sometimes show up at the top of boards, at the bottom of boards, and at the top of searches.
 */
function cleanShopByBanners() {
  waitForKeyElements('div[data-test-id="sf-header-heading"]', function (node) {
    var shopByBannerAtTopOfBoard = node[0].closest(
      'div[class="PKX zI7 iyn Hsu"]',
    );
    if (shopByBannerAtTopOfBoard) {
      shopByBannerAtTopOfBoard.style.display = "none";
      console.debug("Removed shop by banner from top of board");
    }

    if (node[0].closest('div[data-test-id="base-board-pin-grid"]')) {
      var shopByBannerAtBottomOfBoard = node[0].closest(
        'div[class="gcw zI7 iyn Hsu"]',
      );
      if (shopByBannerAtBottomOfBoard) {
        shopByBannerAtBottomOfBoard.style.display = "none";
        console.debug("Removed shop by banner from bottom of board");
      }
    }

    var shopByBannerAtTopOfSearch = node[0].closest('div[role="listitem"]');
    if (shopByBannerAtTopOfSearch) {
      shopByBannerAtTopOfSearch.style.display = "none";
      console.debug("Removed shop by banner from top of search results");
    }
  });
}

// DECLUTTER NAVBAR
function removeBellIcon() {
  if (SETTINGS[SETTING_REMOVE_NAVBAR_NOTIFICATION_ICON].currentValue()) {
    const bellIconDiv = document.querySelector('div[data-test-id="bell-icon"]');
    if (bellIconDiv) {
      bellIconDiv.style.display = "none";
      console.debug("Removed bell icon from navbar.");
    }
  }
}

function removeMessagesIcon() {
  if (SETTINGS[SETTING_REMOVE_NAVBAR_MESSAGES_ICON].currentValue()) {
    const messagesIconDiv = document.querySelector(
      'div[aria-label="Messages"]',
    );
    if (messagesIconDiv) {
      var messagesParent = messagesIconDiv.closest(
        'div[class="XiG zI7 iyn Hsu"]',
      );
      if (messagesParent) {
        messagesParent.style.display = "none";
      }
      console.debug("Removed messages button");
    }
  }
}

/**
function removeNavBarTab(tabName) {
  const navBarTab = document.querySelector(`div[data-test-id="${tabName}"]`);

  navBarTab?.remove();
  console.debug(`Removed navbar tab: ${tabName}`);
}
*/

function removeExploreTabNotificationsIcon() {
  console.debug("Removing notifications icon from Explore tab");
  var exploreTab = document.querySelector('div[data-test-id="today-tab"]');
  if (exploreTab) {
    var notificationsIcon = exploreTab.querySelector(
      'div[aria-label="Notifications"]',
    );

    if (notificationsIcon) {
      notificationsIcon.style.display = "none";
      console.debug("Removed notifications icon from Explore tab");
    }
  }
}

function cleanNavBarCallback(mutationsList) {
  console.debug("Cleaning navbar");
  removeBellIcon();
  removeMessagesIcon();
  var callbackCount = 0;
  waitForKeyElements('div[data-test-id="today-tab"]', function (nodes) {
    new MutationObserver((mutations, observer) => {
      removeExploreTabNotificationsIcon();
    }).observe(nodes[0], {
      childList: true,
      subtree: true,
    });
  });

  removeExploreTabNotificationsIcon();

  // TODO - enable with settings
  // removeNavBarTab("home-tab");
  // removeNavBarTab("today-tab");
  // removeNavBarTab("create-tab");
}

function cleanNavBar() {
  const headerContent = document.getElementById("HeaderContent");
  if (headerContent) {
    var observerCallCount = 0;
    const observer = new MutationObserver((mutations, observer) => {
      cleanNavBarCallback(mutations);
      if (++observerCallCount >= 5) {
        observer.disconnect();
        console.debug("Disconnected cleanNavBar() mutation observer");
      }
    });

    observer.observe(headerContent, {
      childList: true,
      subtree: true,
    });
    cleanNavBarCallback([]);
  }
}

// DECLUTTER PINS
function cleanShoppingAds(pins) {
  const shoppingAdDivs = document.querySelectorAll(
    "div.Ch2.zDA.IZT.CKL.tBJ.dyH.iFc.GTB.H2s",
  );

  shoppingAdDivs.forEach((adDiv) => {
    let parent = adDiv.closest('div[role="listitem"]');
    if (parent) {
      parent.style.display = "none";
      console.debug("Removed shopping container");
    }
  });
}

function cleanIdeasYouMightLove(pins) {
  pins.forEach((pin) => {
    if (
      pin.textContent.toLowerCase().includes("ideas you might love") ||
      pin.textContent.toLowerCase().includes("shop similar") ||
      pin.textContent.toLowerCase().includes("shop featured boards")
    ) {
      pin.style.display = "none";
      console.debug('Removed "Ideas you might love" item:', pin);
    }
  });
}

function removePinFooters(pins) {
  if (!SETTINGS[SETTING_REMOVE_PIN_FOOTERS].currentValue()) {
    return;
  }

  pins.forEach((pin) => {
    const footer = pin.querySelector('div[data-test-id="pinrep-footer"]');
    if (!footer) {
      return;
    }

    if (footer) {
      footer.style.display = "none";
      console.debug("Removed pin footer:", footer);
    }
  });
}

/**
 * Remove Shoppable Pins by looking for a little tag ("Shoppable Pin indicator") in the pin somewhere.
 * Shoppable pin indicators could be in the footer, or as an svg on top of the image
 */
function removeShoppablePins(pins) {
  if (!SETTINGS[SETTING_REMOVE_SHOPPABLE_PINS].currentValue()) {
    return;
  }

  pins.forEach((pin) => {
    const shoppableIndicator = pin.querySelector(
      '[aria-label="Shoppable Pin indicator"]',
    );

    if (shoppableIndicator) {
      pin.style.display = "none";
      console.debug("Removed shoppable pin:", pin);
    }
  });
}

function removeSponsoredPins(pins, mutations) {
  var promotedPinSelector =
    'a[aria-label="Promoted by"], a[aria-label="Promoted by; Opens a new tab"], div[title="Sponsored"]';

  pins.forEach((pin) => {
    if (pin && pin.querySelector(promotedPinSelector)) {
      pin.style.display = "none";
    }
  });

  /*
  if (mutations) {
    mutations.forEach((mutation) => {
      var listItem = mutation.nextSibling?.closest('div[role="listitem"]');
      if (listItem && listItem.querySelector(promotedPinSelector)) {
        listItem.style.display = "none";
      }
    });
  } else {
  }
  */
}

/**
 * Pinterest now has a "Shop now" module as the first result of some searches.
 * We will remove this.
 */
function removeShoppingModule(pins) {
  console.debug("Cleaning shopping modules");
  if (pins) {
    pins.forEach((pin) => {
      var module = pin.querySelector("div.Module");
      if (!module) {
        return;
      }
      if (
        module.innerText &&
        module.innerText.trim().toLowerCase() == "shop now"
      ) {
        pin.style.display = "none";
        console.debug("Removed shopping module");
      }
    });
  }
}

/**
 * Pinterest now is promoting their own shopping boards (from the "Pinterest Shop" user). We
 * will remove those.
 */
function removeFeaturedBoards(pins) {
  if (pins) {
    pins.forEach((pin) => {
      var pinHsu = pin.querySelector('div[class="POt zI7 iyn Hsu"]');
      if (
        pinHsu &&
        pinHsu.innerText &&
        pinHsu.innerText.trim().toLowerCase() == "explore featured boards"
      ) {
        pin.style.display = "none";
        console.debug("Removed featured boards module");
      }
    });
  }
}

function cleanRelatedPinsSection(mutations) {
  console.debug("Cleaning related pins");
  var pins = document.querySelectorAll('div[role="listitem"]');

  cleanIdeasYouMightLove(pins);
  cleanShoppingAds(pins);
  removePinFooters(pins);
  removeShoppablePins(pins);
  removeSponsoredPins(pins, mutations);
  removeShoppingModule(pins);
  removeFeaturedBoards(pins);
}

function cleanProductListingPinPage() {
  waitAndRemove('div[data-test-id="product-price"]');
  waitAndRemove('div[data-test-id="pdp-product-metadata-domain-owner"]');
  waitAndRemove(
    'div[data-test-id="product-shop-button"]',
    (elem) => (elem.parentElement.style.display = "none"),
  );
  waitAndRemove(
    'div[data-test-id="product-description"]',
    (elem) => (elem.parentElement.style.display = "none"),
  );
}

function cleanPinVisualContainer() {
  var pinVisualContainer = document.querySelector(
    'div[data-test-id="closeup-visual-container"]',
  );
  if (pinVisualContainer) {
    var buttonsOnPinImage =
      pinVisualContainer.querySelectorAll('div[role="button"]');
    if (buttonsOnPinImage) {
      for (var i = 0; i < buttonsOnPinImage.length; i++) {
        buttonsOnPinImage[i].style.display = "none";
        console.debug("Hiding button from pin image");
      }
    }

    var shopButton = pinVisualContainer.querySelector(
      'div[data-test-id="experimental-closeup-image-overlay-layer-shop-button"]',
    );
    if (shopButton) {
      shopButton.style.display = "none";
    }
  }
}

/**
 * The collapsible layout seems to be only used for the "Shop the look" section.
 * So we will remove it.
 */
function removeShopTheLookBannerFromPinDescriptionContainer(mutationsList) {
  waitForKeyElements(
    'div[data-test-id="collapsible-layout"]',
    function (nodes) {
      nodes[0].style.display = "none";
      console.debug('Hid "Shop the look" section');
    },
  );
}

function cleanComments() {
  if (SETTINGS[SETTING_REMOVE_COMMENTS].currentValue()) {
    waitForKeyElements("#canonical-card", function (canonicalCards) {
      if (canonicalCards && canonicalCards.length == 1) {
        var canonicalCard = canonicalCards[0];
        if (!canonicalCard) {
          return;
        }
        var hasCommentsHeading =
          canonicalCard.querySelector("#comments-heading") != null;
        if (hasCommentsHeading) {
          canonicalCard.style.display = "none";
          console.debug("Removed comments section from pin");
        }
      }
    });

    waitForKeyElements(
      'div[data-test-id="inline-comment-composer-container"]',
      function (nodes) {
        var commentBox = nodes[0];
        if (commentBox) {
          commentBox.style.display = "none";
          console.debug("Removed comment box from pin");
        }
      },
    );
  }
}

function cleanPinDescriptionContainer() {
  var pinDescriptionContainer = document.querySelector(
    'div[data-test-id="description-content-container"]',
  );

  if (pinDescriptionContainer) {
    var shopButton = pinDescriptionContainer.querySelector(
      'div[data-test-id="product-shop-button"]',
    );
    if (shopButton) {
      shopButton.style.display = "none";
    }

    new MutationObserver(
      removeShopTheLookBannerFromPinDescriptionContainer,
    ).observe(pinDescriptionContainer, {
      childList: true,
      subtree: true,
    });
    removeShopTheLookBannerFromPinDescriptionContainer([]);
  }
}

/**
 * Pinterest adds the number of notifications to the title
 * in a distracting, flashing manner like Pinerest (2).
 * So this will keep the page title at: Pinterest
 */
function enforceTitle() {
  if (document.title !== ORIGINAL_TITLE) {
    console.debug(
      `Changing title from: ${document.title} to ${ORIGINAL_TITLE}`,
    );
    document.title = ORIGINAL_TITLE;
  }
}

/**
 * Pinterest now has a popup modal asking you to disable adblock, if it is present.
 *
 * We will remove this modal.
 */
function removeAntiAdblockModalIfExists() {
  console.debug("Waiting for anti-Adblock modal");
  waitForKeyElements('div[aria-label="Ad blocker modal"]', function (nodes) {
    nodes[0]?.remove();
    document.querySelector('div[name="trap-focus"]')?.remove();
    document.body.style.overflow = "auto";
    console.debug("Removed anti-Adblock modal");
  });
}

// PAGE IDENTIFIERS
function isProductPin() {
  return document.getElementById("product-sticky-container ") != null;
}

function hasDescriptionContentContainer() {
  return (
    document.querySelector(
      'div[data-test-id="description-content-container"]',
    ) != null
  );
}

function isBoard() {
  return document.querySelector('div[data-test-id="board-header"]') != null;
}

function main() {
  "use strict";
  console.debug("Running main()");
  updateSettingsMenu();
  cleanNavBar();
  removeAntiAdblockModalIfExists();
  cleanComments();

  if (isProductPin()) {
    cleanProductListingPinPage();
  }

  cleanShopButtonsFromBoard();
  cleanShopByBanners();

  cleanPinDescriptionContainer();
  cleanPinVisualContainer();
  // The pin visual container changes to add those little "shop" buttons
  // over the top of the image, so we need to watch for those and remove them
  waitForKeyElements(
    'div[data-test-id="closeup-visual-container"]',
    function (nodes) {
      new MutationObserver((mutations, observer) => {
        cleanPinVisualContainer();
      }).observe(nodes[0], {
        childList: true,
        subtree: true,
      });
    },
  );

  const relatedPinsSectionCleanerObserver = new MutationObserver(
    (mutations) => {
      cleanRelatedPinsSection(mutations);
    },
  );

  waitForKeyElements('div[role="list"]', function (node) {
    // Related pins section must be watched for changes,
    // as new pins pop up as the user scrolls
    cleanRelatedPinsSection([]);
    relatedPinsSectionCleanerObserver.observe(node[0], {
      childList: true,
    });
  });

  enforceTitle();
  const titleElement = document.querySelector("title");
  new MutationObserver(enforceTitle).observe(titleElement, { childList: true });
}

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