// ==UserScript==
// @name Pinterest Power Menu
// @description All-in-one Pinterest power tool: original quality, download fixer, closeup image/video downloads, visible text translation, GIF hover/auto-play, remove videos, hide UI elements, declutter, scroll preservation
// @version 1.4.0
// @author Angel
// @namespace https://github.com/Angel2mp3
// @homepageURL https://angelmakes.software
// @icon https://www.pinterest.com/favicon.ico
// @match https://www.pinterest.com/*
// @match https://pinterest.com/*
// @match https://*.pinterest.com/*
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_setClipboard
// @connect *
// @run-at document-start
// @updateURL https://raw.githubusercontent.com/Angel2mp3/Pinterest-Power-Menu/main/PinterestPowerMenu.user.js
// @downloadURL https://raw.githubusercontent.com/Angel2mp3/Pinterest-Power-Menu/main/PinterestPowerMenu.user.js
// ==/UserScript==
(function () {
'use strict';
// ═══════════════════════════════════════════════════════════════════
// SETTINGS
// ═══════════════════════════════════════════════════════════════════
const SETTINGS_KEY = 'pe_settings_v1';
const SCRIPT_VERSION = '1.4.0';
const UPDATE_NOTES_HIGHLIGHTS = [
'Quick Download button on every pin closeup — works for videos too, not just images.',
'Reverse Image Search button on closeups — Google, Yandex, SauceNAO, TinEye.',
'Auto-Play Visible Videos — more reliable, with canplay waits, retries, and tab-visibility resume.',
'New: hover Download button on every pin in feed/search/discovery grids — no closeup needed.',
];
// ── Mobile / touch detection ─────────────────────────────────────────
// Declared early so DEFAULTS can reference it (contextMenu off on mobile).
// Gates features that are mouse-only or cause jank on touch devices.
const IS_MOBILE = /android|iphone|ipad|ipod|mobile/i.test(navigator.userAgent)
|| (navigator.maxTouchPoints > 1 && /macintel/i.test(navigator.platform));
const USER_LANG = ((navigator.language || navigator.userLanguage || 'en').split('-')[0] || 'en').toLowerCase();
function isMobilePinCloseupPage() {
return IS_MOBILE && /\/pin\/\d/i.test(location.pathname);
}
const DEFAULTS = {
originalQuality: true,
downloadFixer: true,
gifHover: true,
hideVisitSite: true,
boardDownloader: true,
declutter: true,
declutterShopTheLook: true,
declutterSearchAdvisory: false,
contextMenu: !IS_MOBILE, // mouse-only feature; off by default on mobile
hideUpdates: false,
hideMessages: false,
hideShare: false,
gifAutoPlay: false,
videoAutoPlay: false,
infiniteLoopVideo: false,
darkMode: 'auto',
removeVideos: false,
hideShopPosts: false,
hideComments: false,
hideCommentButton: false,
hideReactButton: false,
hideReactionCount: false,
hideUploadImageButton: false,
hideSearchImageButton: false,
hideSearchSuggestions: false,
hideViewLargerButton: false,
hideMoreOptionsButton: false,
hideReverseImageSearchButton: false,
hideCommentEmojiButton: false,
hideCommentStickerButton: false,
hideCommentPhotoButton: false,
autoTranslate: false,
autoTranslateTitles: false,
autoTranslateDescriptions: false,
autoTranslateComments: false,
autoTranslateCommentMode: 'visible',
autoTranslateTarget: 'browser',
titleTranslationDisplay: 'translated',
customPinterestLogoUrl: '',
customPinterestLogoSize: 32,
customPinterestLogoCircle: true,
reverseImageSearchButton: true,
updateNotesDisabled: false,
lastUpdateNotesVersion: '',
};
let _cfg = null;
function loadCfg() {
try {
const raw = GM_getValue(SETTINGS_KEY, null);
const saved = raw ? JSON.parse(raw) : {};
_cfg = { ...DEFAULTS, ...saved };
if (saved.autoTranslate === true) {
if (saved.autoTranslateTitles === undefined) _cfg.autoTranslateTitles = true;
if (saved.autoTranslateDescriptions === undefined) _cfg.autoTranslateDescriptions = true;
if (saved.autoTranslateComments === undefined) _cfg.autoTranslateComments = true;
}
if (saved.hideComments === true && saved.hideCommentButton === undefined) _cfg.hideCommentButton = true;
if (saved.autoTranslateTarget === undefined) _cfg.autoTranslateTarget = DEFAULTS.autoTranslateTarget;
if (saved.autoTranslateCommentMode === undefined) _cfg.autoTranslateCommentMode = DEFAULTS.autoTranslateCommentMode;
_cfg.showManualTranslateButtons = false;
rememberMissingDefaultPrefs(saved);
} catch (_) {
_cfg = { ...DEFAULTS };
}
}
function saveCfg() {
try { GM_setValue(SETTINGS_KEY, JSON.stringify(_cfg)); } catch (_) {}
}
function rememberMissingDefaultPrefs(saved) {
let changed = false;
Object.keys(DEFAULTS).forEach(key => {
if (Object.prototype.hasOwnProperty.call(saved, key)) return;
if (_cfg[key] === undefined) _cfg[key] = DEFAULTS[key];
changed = true;
});
if (changed) saveCfg();
}
function get(key) {
if (!_cfg) loadCfg();
return key in _cfg ? _cfg[key] : DEFAULTS[key];
}
function set(key, val) {
if (!_cfg) loadCfg();
_cfg[key] = val;
saveCfg();
}
function shouldShowUpdateNotes() {
return !get('updateNotesDisabled') && get('lastUpdateNotesVersion') !== SCRIPT_VERSION;
}
function escapeUpdateNoteText(value) {
return String(value || '').replace(/[&<>"']/g, ch => ({
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
}[ch]));
}
function dismissUpdateNotesPopup() {
document.getElementById('pe-update-notes-layer')?.remove();
set('lastUpdateNotesVersion', SCRIPT_VERSION);
}
function disableUpdateNotesForever() {
document.getElementById('pe-update-notes-layer')?.remove();
set('updateNotesDisabled', true);
set('lastUpdateNotesVersion', SCRIPT_VERSION);
}
function createUpdateNotesPopup() {
if (!document.body || !shouldShowUpdateNotes()) return;
if (document.getElementById('pe-update-notes-layer')) return;
const layer = document.createElement('div');
layer.id = 'pe-update-notes-layer';
layer.setAttribute('data-pe-ui', 'true');
layer.innerHTML = `
Updated to ${escapeUpdateNoteText(SCRIPT_VERSION)}
What's new
${UPDATE_NOTES_HIGHLIGHTS.map(note => `- ${escapeUpdateNoteText(note)}
`).join('')}
`;
layer.addEventListener('click', e => {
if (e.target === layer) dismissUpdateNotesPopup();
});
layer.querySelector('#pe-update-notes-close')?.addEventListener('click', dismissUpdateNotesPopup);
layer.querySelector('#pe-update-notes-never')?.addEventListener('click', disableUpdateNotesForever);
document.body.appendChild(layer);
try {
const mode = get('darkMode');
let dark = false;
if (mode === 'dark') dark = true;
else if (mode === 'auto' && typeof isPinterestDarkTheme === 'function') dark = isPinterestDarkTheme();
layer.querySelector('#pe-update-notes-card')?.classList.toggle('pe-dark', dark);
} catch (_) {}
}
loadCfg();
function injectEarlyDeclutterStyles() {
if (document.getElementById('pe-declutter-early-styles')) return;
const style = document.createElement('style');
style.id = 'pe-declutter-early-styles';
style.textContent = `
html.pe-declutter-enabled div[role="list"] > div[role="listitem"]:has(div[title="Sponsored"]),
html.pe-declutter-enabled div[role="list"] > div[role="listitem"]:has([aria-label="Shoppable Pin indicator"]),
html.pe-declutter-enabled div[role="list"] > div[role="listitem"]:has([data-test-id="product-price-text"]),
html.pe-declutter-enabled div[role="list"] > div[role="listitem"]:has([data-test-id="pincard-product-with-link"]) {
height: 0 !important;
width: 0 !important;
margin: 0 !important;
padding: 0 !important;
border: none !important;
overflow: hidden !important;
opacity: 0 !important;
min-height: 0 !important;
min-width: 0 !important;
pointer-events: none !important;
}
html.pe-declutter-enabled.pe-declutter-shop-look-enabled [data-test-id="duplo-shopping-module"],
html.pe-declutter-enabled.pe-declutter-shop-look-enabled [data-test-id="ShopTheLookSimilarProducts"],
html.pe-declutter-enabled.pe-declutter-shop-look-enabled [data-test-id="visual-search-shopping-bar"],
html.pe-declutter-enabled.pe-declutter-shop-look-enabled [data-test-id="related-products"],
html.pe-declutter-enabled.pe-declutter-shop-look-enabled [data-test-id="ShopTheLookAnnotations"],
html.pe-declutter-enabled.pe-declutter-shop-look-enabled [data-test-id="shopping-module"] {
height: 0 !important;
width: 0 !important;
margin: 0 !important;
padding: 0 !important;
border: none !important;
overflow: hidden !important;
opacity: 0 !important;
min-height: 0 !important;
min-width: 0 !important;
pointer-events: none !important;
}
html.pe-declutter-enabled.pe-declutter-advisory-enabled [data-test-id="search-advisory"],
html.pe-declutter-enabled.pe-declutter-advisory-enabled [data-test-id="fresh-search-advisory"] {
height: 0 !important;
width: 0 !important;
margin: 0 !important;
padding: 0 !important;
border: none !important;
overflow: hidden !important;
opacity: 0 !important;
min-height: 0 !important;
min-width: 0 !important;
pointer-events: none !important;
}
html.pe-declutter-enabled [data-test-id="pin-action-bar-container"]:has([data-test-id="visit-button-mobile-inline"]),
html.pe-declutter-enabled [data-test-id="visit-button-mobile-inline"],
html.pe-declutter-enabled [data-test-id="main-pin-section-visit-button"] {
height: 0 !important;
width: 0 !important;
margin: 0 !important;
padding: 0 !important;
border: none !important;
overflow: hidden !important;
opacity: 0 !important;
min-height: 0 !important;
min-width: 0 !important;
pointer-events: none !important;
}
`;
(document.head || document.documentElement).appendChild(style);
}
function applyDeclutterToggle() {
document.documentElement.classList.toggle('pe-declutter-enabled', get('declutter'));
document.documentElement.classList.toggle('pe-declutter-shop-look-enabled', get('declutter') && get('declutterShopTheLook'));
document.documentElement.classList.toggle('pe-declutter-advisory-enabled', get('declutter') && get('declutterSearchAdvisory'));
}
injectEarlyDeclutterStyles();
applyDeclutterToggle();
// ─── Video URL interceptor ──────────────────────────────────────────────
// On desktop, Pinterest uses HLS.js which sets video.src to a blob:
// MediaSource URL — findPinterestVideoSrc() cannot read the actual CDN URL
// from the DOM. Intercept XHR/fetch at document-start to capture
// v1.pinimg.com video URLs as they are requested by HLS.js, then use them
// as a fallback for the Quick Download button.
function extractPinterestVideoHashFromText(value) {
const text = String(value || '');
const path = text.match(/(?:^|\/)([a-f0-9]{2})\/([a-f0-9]{2})\/([a-f0-9]{2})\/([a-f0-9]{32})(?=[._/?#]|$)/i);
if (path) return `${path[1].toLowerCase()}/${path[2].toLowerCase()}/${path[3].toLowerCase()}/${path[4].toLowerCase()}`;
const bare = text.match(/\b([a-f0-9]{32})\b/i)?.[1];
if (!bare) return '';
const hash = bare.toLowerCase();
return `${hash.slice(0, 2)}/${hash.slice(2, 4)}/${hash.slice(4, 6)}/${hash}`;
}
function getPinterestVideoCdnBucket(value) {
return String(value || '').match(/v1\.pinimg\.com\/videos\/(mc|iht)\//i)?.[1]?.toLowerCase() || '';
}
const _interceptedVideoUrls = []; // most-recently-seen first
const _interceptedVideoUrlsByHash = new Map();
const _mobilePinVideoDownloadCache = new Map();
let _onVideoUrlCapture = null; // set by Quick Download startup
(function () {
function captureVideoUrl(url) {
if (typeof url !== 'string') return;
if (!/v1\.pinimg\.com\/videos/i.test(url)) return;
const idx = _interceptedVideoUrls.indexOf(url);
if (idx !== -1) _interceptedVideoUrls.splice(idx, 1);
_interceptedVideoUrls.unshift(url); // newest first
if (_interceptedVideoUrls.length > 20) _interceptedVideoUrls.pop();
const hash = extractPinterestVideoHashFromText(url);
if (hash) {
const urls = _interceptedVideoUrlsByHash.get(hash) || [];
const hashIdx = urls.indexOf(url);
if (hashIdx !== -1) urls.splice(hashIdx, 1);
urls.unshift(url);
if (urls.length > 8) urls.pop();
_interceptedVideoUrlsByHash.set(hash, urls);
}
if (typeof _onVideoUrlCapture === 'function') _onVideoUrlCapture();
}
const _xOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function (m, url, ...a) {
captureVideoUrl(String(url));
return _xOpen.call(this, m, url, ...a);
};
const _oFetch = window.fetch;
if (typeof _oFetch === 'function') {
window.fetch = function (input) {
captureVideoUrl(typeof input === 'string' ? input : (input && input.url) || '');
return _oFetch.apply(this, arguments);
};
}
})();
// Utility: returns a debounced version of fn (resets timer on every call).
function debounce(fn, ms) {
let t;
return function () { clearTimeout(t); t = setTimeout(fn, ms); };
}
function isPowerMenuNode(node) {
if (!node || node.nodeType !== 1) return false;
return !!node.closest?.(
'[data-pe-ui="true"], #pe-settings-wrap, #pe-ctx-menu, #pe-bd-fab, ' +
'#pe-reverse-image-search-menu, #pe-toast'
);
}
function isPowerMenuEvent(e) {
return isPowerMenuNode(e?.target);
}
function hasOnlyPowerMenuMutations(records) {
return !!records?.length && records.every(record => {
if (isPowerMenuNode(record.target)) return true;
const nodes = [...record.addedNodes, ...record.removedNodes]
.filter(node => node.nodeType === 1);
return nodes.length > 0 && nodes.every(isPowerMenuNode);
});
}
// ═══════════════════════════════════════════════════════════════════
// MODULE: ORIGINAL QUALITY (fast – no probe, no popup)
// ═══════════════════════════════════════════════════════════════════
// Directly rewrite pinimg.com thumbnail URLs → /originals/ with
// an inline onerror fallback to /736x/ so zero extra requests are
// made upfront and the "Optimizing…" overlay is never shown.
const OQ_RE = /^(https?:\/\/i\.pinimg\.com)\/\d+x(\/[0-9a-f]{2}\/[0-9a-f]{2}\/[0-9a-f]{2}\/[0-9a-f]{32}\.(?:jpg|jpeg|png|gif|webp))$/i;
function upgradeImg(img) {
if (!get('originalQuality')) return;
if (img.__peOQ || img.tagName !== 'IMG' || !img.src) return;
const m = img.src.match(OQ_RE);
if (!m) return;
img.__peOQ = true;
const origSrc = m[1] + '/originals' + m[2];
const fallSrc = m[1] + '/736x' + m[2];
img.onerror = function () {
if (img.src === origSrc) { img.onerror = null; img.src = fallSrc; }
};
if (img.getAttribute('data-src') === img.src) img.setAttribute('data-src', origSrc);
img.src = origSrc;
}
function scanOQ(node) {
if (!node || node.nodeType !== 1) return;
if (node.tagName === 'IMG') upgradeImg(node);
else node.querySelectorAll('img[src*="pinimg.com"]').forEach(upgradeImg);
}
// Start MutationObserver immediately (document-start) so we catch
// images before they fire their first load event.
const oqObs = new MutationObserver(records => {
if (!get('originalQuality')) return;
const process = () => records.forEach(r => {
if (r.attributeName === 'src') upgradeImg(r.target);
else r.addedNodes.forEach(scanOQ);
});
// On mobile, yield to the browser's render pipeline so scroll stays smooth
if (IS_MOBILE && typeof requestIdleCallback === 'function') {
requestIdleCallback(process, { timeout: 300 });
} else {
process();
}
});
oqObs.observe(document.documentElement, {
childList: true, subtree: true,
attributes: true, attributeFilter: ['src'],
});
// ═══════════════════════════════════════════════════════════════════
// MODULE: HIDE VISIT SITE
// ═══════════════════════════════════════════════════════════════════
// Uses CSS classes on so toggles are instant and zero-cost.
function applyVisitSiteToggle() {
if (!document.body) return;
document.body.classList.toggle('pe-hide-visit', get('hideVisitSite'));
}
function applyNavToggles() {
if (!document.body) return;
applyDeclutterToggle();
document.body.classList.toggle('pe-hide-updates', get('hideUpdates'));
document.body.classList.toggle('pe-hide-messages', get('hideMessages'));
document.body.classList.toggle('pe-hide-share', get('hideShare'));
document.body.classList.toggle('pe-hide-comments', get('hideComments'));
document.body.classList.toggle('pe-hide-comment-button', get('hideCommentButton'));
document.body.classList.toggle('pe-hide-react', get('hideReactButton'));
document.body.classList.toggle('pe-hide-reaction-count', get('hideReactionCount'));
document.body.classList.toggle('pe-hide-upload-image', !IS_MOBILE && get('hideUploadImageButton'));
document.body.classList.toggle('pe-hide-search-image', get('hideSearchImageButton'));
document.body.classList.toggle('pe-hide-search-suggestions', get('hideSearchSuggestions'));
document.body.classList.toggle('pe-hide-view-larger', get('hideViewLargerButton'));
document.body.classList.toggle('pe-hide-more-options', get('hideMoreOptionsButton'));
document.body.classList.toggle('pe-hide-reverse-image-search', get('hideReverseImageSearchButton'));
document.body.classList.toggle('pe-hide-comment-emoji', get('hideCommentEmojiButton'));
document.body.classList.toggle('pe-hide-comment-sticker', get('hideCommentStickerButton'));
document.body.classList.toggle('pe-hide-comment-photo', get('hideCommentPhotoButton'));
}
// Physically removes the Messages nav button from the DOM (not just hidden with CSS).
// A MutationObserver re-removes it whenever Pinterest re-renders the nav (SPA navigation).
let _messagesRemoverObs = null;
function initMessagesRemover() {
if (!get('hideMessages')) return;
if (_messagesRemoverObs) return; // already running
const SELS = [
'div[aria-label="Messages"]',
'[data-test-id="nav-bar-speech-ellipsis"]',
];
function removeNow(root) {
SELS.forEach(sel => {
(root.querySelectorAll ? root.querySelectorAll(sel) : []).forEach(el => el.remove());
});
}
removeNow(document);
_messagesRemoverObs = new MutationObserver(recs => {
if (hasOnlyPowerMenuMutations(recs)) return;
if (!get('hideMessages')) { _messagesRemoverObs.disconnect(); _messagesRemoverObs = null; return; }
recs.forEach(r => r.addedNodes.forEach(n => { if (n.nodeType === 1) removeNow(n); }));
});
_messagesRemoverObs.observe(document.documentElement, { childList: true, subtree: true });
}
// JS-based "Visit site" link removal – catches links that CSS alone misses
// (e.g. Visit site
)
function initVisitSiteHider() {
function hideInTree(root) {
if (!get('hideVisitSite') || !root) return;
const links = root.querySelectorAll ? root.querySelectorAll('a') : [];
links.forEach(a => {
if (a.__peVisitHidden) return;
const text = a.textContent.trim();
if (/^visit\s*site$/i.test(text)) {
a.__peVisitHidden = true;
a.style.setProperty('display', 'none', 'important');
}
});
}
hideInTree(document);
new MutationObserver(recs => {
if (hasOnlyPowerMenuMutations(recs)) return;
if (!get('hideVisitSite')) return;
recs.forEach(r => r.addedNodes.forEach(n => {
if (n.nodeType === 1) hideInTree(n);
}));
}).observe(document.documentElement, { childList: true, subtree: true });
}
// ═══════════════════════════════════════════════════════════════════
// MODULE: SHARE URL OVERRIDE
// ═══════════════════════════════════════════════════════════════════
// Replaces Pinterest's shortened pin.it URLs in the share dialog
// with the actual pin URL. On closeup pages that's location.href;
// on the grid we walk up from the share button to find the pin link.
// Also intercepts "Copy link" and clicks on the URL input box.
function initShareOverride() {
const nativeSetter = Object.getOwnPropertyDescriptor(
HTMLInputElement.prototype, 'value'
).set;
let _sharePinUrl = null;
// 1) Track share/send button clicks to capture the pin's real URL
document.addEventListener('click', e => {
if (isPowerMenuEvent(e)) return;
const shareBtn = e.target.closest(
'[data-test-id="sendPinButton"], button[aria-label="Send"], ' +
'[data-test-id="closeup-share-button"], div[aria-label="Share"], ' +
'button[aria-label="Share"]'
);
if (!shareBtn) return;
// On a pin closeup page, location.href IS the pin URL
if (/\/pin\/\d+/.test(location.pathname)) {
_sharePinUrl = location.href;
return;
}
// On grid: walk up from the share button to find the pin card link
_sharePinUrl = null;
let el = shareBtn;
for (let i = 0; i < 30 && el; i++) {
if (el.querySelector) {
const link = el.querySelector('a[href*="/pin/"]');
if (link) {
_sharePinUrl = new URL(link.href, location.origin).href;
break;
}
}
el = el.parentElement;
}
if (!_sharePinUrl) _sharePinUrl = location.href;
}, true);
// 2) Watch for the share-popup URL input and override its value
function fixShareInputs() {
const realUrl = _sharePinUrl || location.href;
document.querySelectorAll(
'input#url-text, ' +
'[data-test-id="copy-link-share-icon-auth"] input[type="text"], ' +
'input[readonly][value*="pin.it"], ' +
'input[readonly][value*="pinterest.com/pin/"]'
).forEach(input => {
// Always re-fix if value doesn't match
if (input.value !== realUrl) {
nativeSetter.call(input, realUrl);
input.dispatchEvent(new Event('input', { bubbles: true }));
}
if (!input.__peShareClick) {
input.__peShareClick = true;
// Intercept clicks on the input box itself
input.addEventListener('click', ev => {
ev.stopPropagation();
const url = _sharePinUrl || location.href;
navigator.clipboard.writeText(url).catch(() => {
const ta = document.createElement('textarea');
ta.value = url;
ta.style.cssText = 'position:fixed;left:-9999px';
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
ta.remove();
});
}, true);
// Re-fix if React re-renders the value
new MutationObserver(() => {
const url = _sharePinUrl || location.href;
if (input.value !== url) {
nativeSetter.call(input, url);
input.dispatchEvent(new Event('input', { bubbles: true }));
}
}).observe(input, { attributes: true, attributeFilter: ['value'] });
}
});
}
new MutationObserver(records => {
if (hasOnlyPowerMenuMutations(records)) return;
fixShareInputs();
})
.observe(document.documentElement, { childList: true, subtree: true });
// 3) Intercept "Copy link" button clicks
document.addEventListener('click', e => {
if (isPowerMenuEvent(e)) return;
const copyBtn = e.target.closest(
'button[aria-label="Copy link"], ' +
'[data-test-id="copy-link-share-icon-auth"] button'
);
if (!copyBtn) return;
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
const realUrl = _sharePinUrl || location.href;
navigator.clipboard.writeText(realUrl).then(() => {
const txt = copyBtn.querySelector('div');
if (txt) {
const orig = txt.textContent;
txt.textContent = 'Copied!';
setTimeout(() => { txt.textContent = orig; }, 1500);
}
}).catch(() => {
const ta = document.createElement('textarea');
ta.value = realUrl;
ta.style.cssText = 'position:fixed;left:-9999px';
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
ta.remove();
});
}, true);
}
// ═══════════════════════════════════════════════════════════════════
// MODULE: GIF / VIDEO HOVER PLAY
// ═══════════════════════════════════════════════════════════════════
// In the pin grid, Pinterest renders GIFs as static
elements
// (showing a .jpg thumbnail) with the real .gif URL hidden in
// srcset at "4x". There is no