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