☰

PMVHaven

Restores native context menus, hides known intrusive ad UI, consolidates chunked video grids.

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==UserScript==
// @name         PMVHaven
// @namespace    https://pmvhaven.com/
// @version      1.3.0
// @description  Restores native context menus, hides known intrusive ad UI, consolidates chunked video grids.
// @match        https://pmvhaven.com/*
// @match        https://*.pmvhaven.com/*
// @run-at       document-start
// @sandbox      raw
// @noframes
// @author       Cat-Ling
// @license      MIT
// ==/UserScript==

(() => {
  'use strict';

  const STYLE_ID = 'pmvhaven-optimizer-styles-v110';
  const BLOCKED_MENU_MARKER = 'data-pmvhaven-hidden-context-menu';

  /*
   * Evidence-backed menu fingerprint from PMVHaven's captured VideoCard chunk:
   *   div.fixed ... z-[100] min-w-[200px] overflow-hidden
   *     > a[target="_blank"][rel~="noopener"]
   *       > span "Open in new tab"
   *
   * Vue attaches its own `contextmenu` listener with a prevent modifier. The
   * window capture listener below stops that listener before it can cancel the
   * browser default. The DOM/CSS fallback catches the menu if another handler,
   * long-press path, or site update still manages to create it.
   */
  const VIDEO_CONTEXT_MENU_SELECTOR =
    'div.fixed[class~="z-[100]"][class~="min-w-[200px]"].overflow-hidden:has(> a[target="_blank"][rel~="noopener"] > span)';

  const AD_SELECTORS = [
    // Confirmed from the captured NativeAdCard / InlineScrollBanner chunks.
    'a.native-ad-card:not([data-video-id])',
    'a.ad-link:not([data-video-id])',
    'a[rel~="sponsored"]:not([data-video-id])',
    'a.inline-scroll-banner:not([data-video-id])',

    // Site-specific header/mobile ad variants retained from v1.0.0.
    'a.undress-ai-animated-btn:not([data-video-id])',
    'a[href*="happyleafmotion.com"]:not([data-video-id])',
    'a[href*="s.eunow4u.com"]:not([data-video-id])',
    'a[href*="rr.rndomnm.com"]:not([data-video-id])',
    'a[href*="fluxion-lab"]:not([data-video-id])',
    'a[href*="realxxx.com"]:not([data-video-id])',
    '.mobile-undress-ad-container',
    '.mobile-undress-ad-banner',
    '.mobile-header-ad',
    '.ads-container',
    '.video-player-banner',
    '.sidebar-ad-container',
    '.sidebar-ad-wrapper',
    '.sidebar-ad-iframe',
    '.ad-banner',
    '.ad-content-mobile',

    // Known ad/creative iframe hosts retained as defensive fallbacks.
    'iframe[src*="sadbaguette.com"]',
    'iframe[src*="clearsky-rhythm.com"]',
    'iframe[src*="exacdn.com"]',
    'iframe[src*="happyleafmotion.com"]',
    'iframe[src*="fluxion-lab"]',
    'iframe[src*="rr.rndomnm.com"]',
    'iframe[src*="s.eunow4u.com"]',
    'iframe[src*="realxxx.com"]'
  ];

  const AD_CLICK_SELECTOR = [
    'a.native-ad-card',
    'a.ad-link',
    'a[rel~="sponsored"]',
    'a.inline-scroll-banner',
    'a.undress-ai-animated-btn',
    'a[href*="happyleafmotion.com"]',
    'a[href*="s.eunow4u.com"]',
    'a[href*="rr.rndomnm.com"]',
    'a[href*="fluxion-lab"]',
    'a[href*="realxxx.com"]',
    '[class*="native-ad"]',
    '[class*="inline-scroll-banner"]'
  ].join(',');

  const GRID_WRAPPER_SELECTOR = [
    'div:has(> .grid[class~="cols-4-1500"])',
    'div:has(> .grid[class~="cols-3-1200"])'
  ].join(',\n');

  const GRID_CHUNK_SELECTOR = [
    'div:has(> .grid[class~="cols-4-1500"]) > .grid',
    'div:has(> .grid[class~="cols-3-1200"]) > .grid'
  ].join(',\n');

  const GRID_SPACER_SELECTOR = [
    'div:has(> .grid[class~="cols-4-1500"]) > div.my-6:not(:has([data-video-id]))',
    'div:has(> .grid[class~="cols-3-1200"]) > div.my-6:not(:has([data-video-id]))'
  ].join(',\n');

  const css = `
    /* Hide known ad UI without hiding real video cards. */
    ${AD_SELECTORS.join(',\n    ')} {
      display: none !important;
      visibility: hidden !important;
      pointer-events: none !important;
    }

    /* The site uses a custom Vue/Teleport menu, not a Radix/Reka menu. */
    ${VIDEO_CONTEXT_MENU_SELECTOR},
    [data-reka-context-menu-content],
    [data-radix-context-menu-content] {
      display: none !important;
      visibility: hidden !important;
      pointer-events: none !important;
    }

    /* Merge the split video-grid chunks into one responsive grid. */
    ${GRID_WRAPPER_SELECTOR} {
      display: grid !important;
      grid-template-columns: repeat(1, minmax(0, 1fr)) !important;
      gap: 1rem !important;
      align-items: stretch !important;
    }

    @media (min-width: 640px) {
      ${GRID_WRAPPER_SELECTOR} {
        grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
      }
    }

    @media (min-width: 1200px) and (max-width: 1499.98px) {
      ${GRID_WRAPPER_SELECTOR} {
        grid-template-columns: repeat(3, minmax(0, 1fr)) !important;
      }
    }

    @media (min-width: 1500px) {
      ${GRID_WRAPPER_SELECTOR} {
        grid-template-columns: repeat(4, minmax(0, 1fr)) !important;
      }
    }

    /* Let individual video cards participate directly in the outer grid. */
    ${GRID_CHUNK_SELECTOR} {
      display: contents !important;
      contain: none !important;
    }

    /* Only remove the empty inter-chunk spacer inside the affected wrapper. */
    ${GRID_SPACER_SELECTOR} {
      display: none !important;
      margin: 0 !important;
      padding: 0 !important;
      height: 0 !important;
      min-height: 0 !important;
    }

    /* Reduce backdrop-filter work on video-card overlays only. Do not reset
       transforms or global transitions: the site uses those for card/preview UI. */
    a[data-video-id] [class*="backdrop-blur"] {
      backdrop-filter: none !important;
      -webkit-backdrop-filter: none !important;
    }
  `;

  function injectStyles() {
    if (document.getElementById(STYLE_ID)) return;

    const target = document.head || document.documentElement;
    if (!target) return;

    const style = document.createElement('style');
    style.id = STYLE_ID;
    style.textContent = css;
    target.appendChild(style);
  }

  // Do not call preventDefault(): that would suppress the native browser menu.
  function restoreNativeContextMenu(event) {
    event.stopImmediatePropagation();
  }

  window.addEventListener('contextmenu', restoreNativeContextMenu, true);
  // Defensive fallback for synthetic/non-composed events that do not reach window.
  document.addEventListener('contextmenu', restoreNativeContextMenu, true);

  // Keep core menu restoration alive even if a browser/manager blocks API hooks.
  try {
    blockObservedPopunderRequest();
  } catch (error) {
    console.warn('[PMVHaven] Could not install the narrow popunder request guard:', error);
  }

  function normalizeText(value) {
    return String(value || '').replace(/\s+/g, ' ').trim();
  }

  function hideSiteVideoContextMenu(anchor) {
    if (!(anchor instanceof HTMLAnchorElement)) return;
    if (normalizeText(anchor.textContent) !== 'Open in new tab') return;
    if (anchor.target !== '_blank' || !anchor.relList.contains('noopener')) return;

    let url;
    try {
      url = new URL(anchor.href, location.href);
    } catch {
      return;
    }

    // VideoCard links in the captured site use /video/<slug>_<id> routes.
    if (url.origin !== location.origin || !url.pathname.startsWith('/video/')) return;

    let candidate = anchor.parentElement;
    for (let depth = 0; candidate && depth < 4; depth += 1, candidate = candidate.parentElement) {
      const classes = candidate.classList;
      if (
        classes.contains('fixed') &&
        classes.contains('overflow-hidden') &&
        classes.contains('z-[100]') &&
        classes.contains('min-w-[200px]')
      ) {
        if (!candidate.hasAttribute(BLOCKED_MENU_MARKER)) {
          candidate.setAttribute(BLOCKED_MENU_MARKER, '');
          candidate.setAttribute('aria-hidden', 'true');
          candidate.setAttribute('inert', '');
          candidate.style.setProperty('display', 'none', 'important');
          candidate.style.setProperty('visibility', 'hidden', 'important');
          candidate.style.setProperty('pointer-events', 'none', 'important');
        }
        return;
      }
    }
  }

  function inspectForMenus(root) {
    if (!(root instanceof Element || root instanceof Document)) return;

    if (root instanceof HTMLAnchorElement) hideSiteVideoContextMenu(root);

    if (typeof root.querySelectorAll === 'function') {
      root.querySelectorAll('a[target="_blank"][rel~="noopener"]').forEach(hideSiteVideoContextMenu);
    }
  }

  /*
   * The captured network trace records GET https://s.eunow4u.com/venor.php
   * from a preparePop initiator while the site's ad config enables an
   * inline-v6 popunder. Block only that exact observed endpoint, not the whole
   * host or ordinary fetch/XHR traffic. @sandbox raw makes these hooks run in
   * the page's JS world in Tampermonkey, where the site's requests originate.
   */
  function isObservedPopunderEndpoint(candidate) {
    try {
      const raw = candidate instanceof Request ? candidate.url : String(candidate);
      const url = new URL(raw, location.href);
      return url.hostname.toLowerCase() === 's.eunow4u.com' && url.pathname === '/venor.php';
    } catch {
      return false;
    }
  }

  function blockObservedPopunderRequest() {
    const nativeFetch = window.fetch;
    if (typeof nativeFetch === 'function') {
      window.fetch = function pmvhavenFetchGuard(input, init) {
        if (isObservedPopunderEndpoint(input)) {
          return Promise.reject(new DOMException('Blocked known PMVHaven popunder endpoint', 'AbortError'));
        }
        return Reflect.apply(nativeFetch, this, arguments);
      };
    }

    const xhrPrototype = XMLHttpRequest.prototype;
    const nativeOpen = xhrPrototype.open;
    const nativeSend = xhrPrototype.send;
    const blockedRequests = new WeakSet();

    xhrPrototype.open = function pmvhavenXHROpenGuard(method, url, ...rest) {
      if (isObservedPopunderEndpoint(url)) blockedRequests.add(this);
      else blockedRequests.delete(this);
      return Reflect.apply(nativeOpen, this, [method, url, ...rest]);
    };

    xhrPrototype.send = function pmvhavenXHRSendGuard(...args) {
      if (blockedRequests.has(this)) {
        blockedRequests.delete(this);
        // XHR.open does not send anything; returning here prevents the network
        // request entirely. Emit the failure events that an ad-specific network
        // error would normally produce, without inventing a successful response.
        const xhr = this;
        queueMicrotask(() => {
          try {
            xhr.dispatchEvent(new ProgressEvent('error', {
              lengthComputable: false,
              loaded: 0,
              total: 0
            }));
            xhr.dispatchEvent(new ProgressEvent('loadend', {
              lengthComputable: false,
              loaded: 0,
              total: 0
            }));
          } catch {
            // The request is already suppressed; event synthesis is best-effort.
          }
        });
        return undefined;
      }
      return Reflect.apply(nativeSend, this, args);
    };
  }

  function interceptAdClicks(event) {
    const target = event.target instanceof Element
      ? event.target
      : event.target instanceof Node
        ? event.target.parentElement
        : null;
    if (!target) return;

    // Explicitly protect legitimate video cards, including their nested controls.
    if (target.closest('a[data-video-id]')) return;

    const ad = target.closest(AD_CLICK_SELECTOR);
    if (!ad || ad.hasAttribute('data-video-id')) return;

    event.preventDefault();
    event.stopImmediatePropagation();
  }

  window.addEventListener('click', interceptAdClicks, true);

  // Handles Vue/Teleport menus and ads that appear after the initial page render.
  const observer = new MutationObserver((records) => {
    injectStyles();
    for (const record of records) {
      for (const node of record.addedNodes) inspectForMenus(node);
    }
  });
  observer.observe(document, { childList: true, subtree: true });

  injectStyles();
  inspectForMenus(document);
})();