Greasy Fork is available in English.

X Reading Enhancer

Improve x.com reading by toggling side columns, ads, and media defaults.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

Advertisement:

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

Advertisement:

// ==UserScript==
// @name         X Reading Enhancer
// @name:zh-CN   X 阅读增强
// @namespace    https://github.com/local/x-reading-enhancer
// @version      0.4.1
// @description  Improve x.com reading by toggling side columns, ads, and media defaults.
// @description:zh-CN 优化 x.com / twitter.com 阅读体验:隐藏左右栏、广告,控制媒体默认展示,并提供快捷键和可拖动悬浮面板。
// @author       Codex
// @match        https://x.com/*
// @match        https://twitter.com/*
// @license      MIT
// @supportURL   https://github.com/Skylerliutian/x-reading-enhancer/issues
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @run-at       document-start
// ==/UserScript==

(function () {
  "use strict";

  const STORAGE_PREFIX = "x-reading-enhancer.";
  const DEFAULTS = {
    leftVisible: true,
    rightVisible: true,
    hideAds: true,
    mediaVisible: true,
    panelOpen: false,
    panelPosition: null,
  };
  const DRAG_MARGIN = 8;
  const DRAG_CLICK_TOLERANCE = 4;
  const PANEL_OFFSET = 22;
  const SHORTCUTS = {
    A: "hideAds",
    L: "leftVisible",
    M: "mediaVisible",
    R: "rightVisible",
  };

  const AD_PATTERNS = [
    /^ad$/i,
    /^promoted$/i,
    /^promoted by\b/i,
    /^sponsored$/i,
    /^sponsored by\b/i,
    /^广告$/,
    /^廣告$/,
    /^推广$/,
    /^推廣$/,
    /^赞助$/,
    /^贊助$/,
    /^プロモーション$/,
    /^広告$/,
    /^プロモーション広告$/,
    /^gesponsert$/i,
    /^anzeige$/i,
    /^sponsorise$/i,
    /^sponsorise par\b/i,
    /^promocionado$/i,
    /^promovido$/i,
    /^promosso$/i,
    /^annuncio$/i,
    /^reklama$/i,
    /^реклама$/i,
  ];

  const MEDIA_SELECTORS = [
    '[data-testid="tweetPhoto"]',
    '[data-testid="videoPlayer"]',
    '[data-testid="videoComponent"]',
    '[data-testid="gifPlayer"]',
    '[data-testid="playButton"]',
    'a[href*="/photo/"]',
    'a[href*="/video/"]',
    'img[src*="pbs.twimg.com/media"]',
    'img[src*="pbs.twimg.com/amplify_video_thumb"]',
    "video",
  ].join(",");
  const MEDIA_BLOCK_SELECTORS = [
    '[data-testid="tweetPhoto"]',
    '[data-testid="videoPlayer"]',
    '[data-testid="videoComponent"]',
    '[data-testid="gifPlayer"]',
    'a[href*="/photo/"]',
    'a[href*="/video/"]',
  ].join(",");

  const settings = loadSettings();
  let observer = null;
  let scanQueued = false;
  let mediaToggleListenerInstalled = false;

  installStyles();
  applyRootClasses();
  onReady(() => {
    ensurePanel();
    setupMediaToggleListener();
    scanPage();
    startObserver();
  });

  function loadSettings() {
    return Object.fromEntries(
      Object.entries(DEFAULTS).map(([key, fallback]) => [key, readSetting(key, fallback)]),
    );
  }

  function readSetting(key, fallback) {
    const storageKey = STORAGE_PREFIX + key;

    try {
      if (typeof GM_getValue === "function") {
        return GM_getValue(storageKey, fallback);
      }
    } catch (_) {
      // Fall through to localStorage.
    }

    try {
      const stored = window.localStorage.getItem(storageKey);
      return stored == null ? fallback : JSON.parse(stored);
    } catch (_) {
      return fallback;
    }
  }

  function writeSetting(key, value) {
    settings[key] = value;
    const storageKey = STORAGE_PREFIX + key;

    try {
      if (typeof GM_setValue === "function") {
        GM_setValue(storageKey, value);
        return;
      }
    } catch (_) {
      // Fall through to localStorage.
    }

    try {
      window.localStorage.setItem(storageKey, JSON.stringify(value));
    } catch (_) {
      // Ignore storage failures; current-page controls still work.
    }
  }

  function installStyles() {
    const css = `
      :root.xre-hide-left header[role="banner"] {
        visibility: hidden !important;
      }

      :root.xre-hide-right [data-testid="sidebarColumn"],
      :root.xre-hide-right aside[role="complementary"] {
        visibility: hidden !important;
      }

      :root.xre-hide-media article:not([data-xre-media-expanded="true"]) [data-testid="tweetPhoto"],
      :root.xre-hide-media article:not([data-xre-media-expanded="true"]) [data-testid="videoPlayer"],
      :root.xre-hide-media article:not([data-xre-media-expanded="true"]) [data-testid="videoComponent"],
      :root.xre-hide-media article:not([data-xre-media-expanded="true"]) [data-testid="gifPlayer"],
      :root.xre-hide-media article:not([data-xre-media-expanded="true"]) [data-testid="playButton"],
      :root.xre-hide-media article:not([data-xre-media-expanded="true"]) a[href*="/photo/"],
      :root.xre-hide-media article:not([data-xre-media-expanded="true"]) a[href*="/video/"],
      :root.xre-hide-media article:not([data-xre-media-expanded="true"]) img[src*="pbs.twimg.com/media"],
      :root.xre-hide-media article:not([data-xre-media-expanded="true"]) img[src*="pbs.twimg.com/amplify_video_thumb"],
      :root.xre-hide-media article:not([data-xre-media-expanded="true"]) video {
        display: none !important;
      }

      .xre-media-toggle {
        display: none;
        width: 100%;
        min-height: 42px;
        margin: 10px 0 2px;
        padding: 10px 12px;
        border: 1px solid rgb(47, 51, 54);
        border-radius: 8px;
        background: rgba(29, 155, 240, 0.1);
        color: #1d9bf0;
        cursor: pointer;
        font: inherit;
        font-weight: 700;
        text-align: center;
      }

      .xre-media-toggle:hover,
      .xre-media-toggle:focus-visible {
        background: rgba(29, 155, 240, 0.18);
        outline: 2px solid rgba(29, 155, 240, 0.5);
        outline-offset: 2px;
      }

      :root.xre-hide-media article[data-xre-has-media="true"]:not([data-xre-media-expanded="true"]) .xre-media-toggle {
        display: block;
      }

      :root:not(.xre-hide-media) .xre-media-toggle {
        display: none !important;
      }

      article[data-xre-ad-hidden="true"] {
        display: none !important;
      }

      #xre-root {
        --xre-panel-offset: ${PANEL_OFFSET}px;
        position: fixed;
        right: 18px;
        bottom: 18px;
        width: 55px;
        height: 55px;
        z-index: 2147483647;
        color-scheme: dark;
        color: #eff3f4;
        scrollbar-color: rgb(62, 65, 68) rgb(22, 24, 28);
        font-family: TwitterChirp, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
        font-size: 14px;
        line-height: 1.35;
        -webkit-text-size-adjust: 100%;
        -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
      }

      #xre-root.xre-positioned {
        right: auto !important;
        bottom: auto !important;
      }

      #xre-root,
      #xre-root * {
        box-sizing: border-box;
      }

      #xre-root .xre-fab {
        display: flex;
        flex-basis: auto;
        flex-direction: column;
        flex-shrink: 0;
        align-self: flex-end;
        align-items: center;
        justify-content: center;
        width: 55px;
        height: 55px;
        min-width: 0;
        min-height: 0;
        margin: 0;
        padding: 0;
        border: 0 solid black;
        border-width: 1px;
        border-color: rgb(75, 78, 82);
        border-radius: 16px;
        background-color: rgba(0, 0, 0, 0.65);
        color: inherit;
        color-scheme: dark;
        box-shadow:
          rgba(255, 255, 255, 0.2) 0 0 15px,
          rgba(255, 255, 255, 0.15) 0 0 3px 1px;
        backdrop-filter: blur(12px);
        -webkit-backdrop-filter: blur(12px);
        box-sizing: border-box;
        cursor: pointer;
        font-family: inherit;
        font-size: inherit;
        font-weight: 800;
        list-style: none;
        outline-style: none;
        pointer-events: auto !important;
        text-align: inherit;
        text-decoration: none;
        transition-duration: 0.2s;
        transition-property: background-color, box-shadow;
        z-index: 0;
        touch-action: none;
      }

      #xre-root .xre-fab:hover,
      #xre-root .xre-fab:focus-visible {
        background-color: rgba(22, 24, 28, 0.75);
        box-shadow:
          rgba(255, 255, 255, 0.28) 0 0 18px,
          rgba(255, 255, 255, 0.18) 0 0 4px 1px;
      }

      #xre-root .xre-fab-icon {
        display: block;
        width: 27px;
        height: 27px;
        color: #eff3f4;
        pointer-events: none;
      }

      #xre-root .xre-visually-hidden {
        position: absolute;
        width: 1px;
        height: 1px;
        margin: -1px;
        padding: 0;
        border: 0;
        overflow: hidden;
        clip: rect(0 0 0 0);
        white-space: nowrap;
      }

      #xre-root .xre-panel {
        position: absolute;
        right: var(--xre-panel-offset);
        bottom: var(--xre-panel-offset);
        width: 238px;
        padding: 14px;
        border: 1px solid rgba(239, 243, 244, 0.18);
        border-radius: 8px;
        background: rgba(0, 0, 0, 0.94);
        box-shadow: 0 10px 36px rgba(0, 0, 0, 0.45);
      }

      #xre-root.xre-panel-below .xre-panel {
        top: var(--xre-panel-offset);
        right: var(--xre-panel-offset);
        bottom: auto;
      }

      #xre-root.xre-panel-right .xre-panel {
        right: auto;
        left: var(--xre-panel-offset);
        bottom: var(--xre-panel-offset);
      }

      #xre-root.xre-panel-right.xre-panel-below .xre-panel {
        right: auto;
        left: var(--xre-panel-offset);
        top: var(--xre-panel-offset);
        bottom: auto;
      }

      #xre-root .xre-panel[hidden] {
        display: none !important;
      }

      #xre-root .xre-title {
        margin: 0 0 10px;
        color: #eff3f4;
        font-size: 15px;
        font-weight: 800;
        text-align: center;
        cursor: grab;
        user-select: none;
      }

      #xre-root.xre-dragging,
      #xre-root.xre-dragging * {
        cursor: grabbing !important;
        user-select: none !important;
      }

      #xre-root .xre-row {
        display: grid;
        grid-template-columns: 24px minmax(0, 1fr) 24px;
        align-items: center;
        min-height: 34px;
        gap: 12px;
        color: #eff3f4;
        cursor: pointer;
        user-select: none;
      }

      #xre-root .xre-row + .xre-row {
        border-top: 1px solid rgba(239, 243, 244, 0.12);
      }

      #xre-root .xre-row > span {
        display: flex;
        flex-direction: column;
        align-items: center;
        gap: 2px;
        grid-column: 2;
        min-width: 0;
        text-align: center;
        overflow-wrap: anywhere;
      }

      #xre-root .xre-shortcut {
        color: #8b98a5;
        font-size: 11px;
        font-weight: 500;
        line-height: 1.1;
      }

      #xre-root .xre-row input {
        grid-column: 3;
        justify-self: end;
        width: 18px;
        height: 18px;
        margin: 0;
        accent-color: #1d9bf0;
      }

      @media (max-width: 720px) {
        #xre-root {
          right: 12px;
          bottom: 12px;
        }

        #xre-root .xre-panel {
          width: min(238px, calc(100vw - 24px));
        }
      }
    `;

    try {
      if (typeof GM_addStyle === "function") {
        GM_addStyle(css);
        return;
      }
    } catch (_) {
      // Fall through to a style element.
    }

    const style = document.createElement("style");
    style.id = "xre-style";
    style.textContent = css;
    (document.head || document.documentElement).appendChild(style);
  }

  function applyRootClasses() {
    const root = document.documentElement;
    root.classList.toggle("xre-hide-left", !settings.leftVisible);
    root.classList.toggle("xre-hide-right", !settings.rightVisible);
    root.classList.toggle("xre-hide-media", !settings.mediaVisible);
  }

  function onReady(callback) {
    if (document.body) {
      callback();
      return;
    }

    document.addEventListener("DOMContentLoaded", callback, { once: true });
  }

  function ensurePanel() {
    if (!document.body || document.getElementById("xre-root")) {
      return;
    }

    const root = document.createElement("div");
    root.id = "xre-root";
    root.innerHTML = `
      <button class="xre-fab" type="button" aria-label="X" aria-controls="xre-panel" aria-expanded="false" title="X">
        <svg class="xre-fab-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
          <path fill="currentColor" d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817-5.97 6.817H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"></path>
        </svg>
        <span class="xre-visually-hidden">X</span>
      </button>
      <section class="xre-panel" id="xre-panel" aria-label="X 阅读增强控制面板">
        <div class="xre-title">阅读控制</div>
        ${renderToggle("leftVisible", "显示左侧栏", "Shift+L")}
        ${renderToggle("rightVisible", "显示右侧栏", "Shift+R")}
        ${renderToggle("hideAds", "隐藏广告", "Shift+A")}
        ${renderToggle("mediaVisible", "默认显示媒体", "Shift+M")}
      </section>
    `;

    document.body.appendChild(root);

    const fab = root.querySelector(".xre-fab");
    const panel = root.querySelector(".xre-panel");
    const title = root.querySelector(".xre-title");
    let suppressFabClickUntil = 0;

    fab.addEventListener("click", (event) => {
      event.stopPropagation();

      if (Date.now() < suppressFabClickUntil) {
        return;
      }

      writeSetting("panelOpen", !settings.panelOpen);
      updatePanel(root);
    });

    root.addEventListener("change", (event) => {
      const input = event.target;

      if (!input || !input.matches || !input.matches("input[data-xre-key]")) {
        return;
      }

      writeSetting(input.dataset.xreKey, input.checked);
      applyRootClasses();
      scanPage();
      updatePanel(root);
    });

    document.addEventListener("keydown", (event) => {
      if (handleShortcut(event, root)) {
        return;
      }

      if (event.key !== "Escape" || panel.hidden) {
        return;
      }

      writeSetting("panelOpen", false);
      updatePanel(root);
      fab.focus();
    }, true);

    document.addEventListener("click", (event) => {
      if (panel.hidden || root.contains(event.target)) {
        return;
      }

      writeSetting("panelOpen", false);
      updatePanel(root);
    }, true);

    applyPanelPosition(root);
    setupPanelDrag(root, [fab, title], (handle) => {
      if (handle === fab) {
        suppressFabClickUntil = Date.now() + 250;
      }
    });
    updatePanel(root);
  }

  function renderToggle(key, label, shortcut) {
    return `
      <label class="xre-row" title="快捷键:${shortcut}">
        <span>
          <span>${label}</span>
          <span class="xre-shortcut">${shortcut}</span>
        </span>
        <input type="checkbox" data-xre-key="${key}">
      </label>
    `;
  }

  function handleShortcut(event, root) {
    if (
      !event.shiftKey ||
      event.ctrlKey ||
      event.altKey ||
      event.metaKey ||
      shouldIgnoreShortcutTarget(event.target)
    ) {
      return false;
    }

    const settingKey = SHORTCUTS[String(event.key || "").toUpperCase()];

    if (!settingKey) {
      return false;
    }

    event.preventDefault();
    event.stopPropagation();
    event.stopImmediatePropagation();
    writeSetting(settingKey, !settings[settingKey]);
    applyRootClasses();
    scanPage();
    updatePanel(root);
    return true;
  }

  function shouldIgnoreShortcutTarget(target) {
    if (!target || !target.closest) {
      return false;
    }

    return Boolean(
      target.closest(
        'input, textarea, select, [contenteditable="true"], [contenteditable=""], [role="textbox"]',
      ),
    );
  }

  function setupMediaToggleListener() {
    if (mediaToggleListenerInstalled) {
      return;
    }

    mediaToggleListenerInstalled = true;
    document.addEventListener("click", (event) => {
      const button = event.target?.closest?.(".xre-media-toggle");

      if (!button) {
        return;
      }

      const article = button.closest('article[data-testid="tweet"], article[role="article"]');

      if (!article) {
        return;
      }

      event.preventDefault();
      event.stopPropagation();
      event.stopImmediatePropagation();
      const expanded = article.dataset.xreMediaExpanded === "true";

      if (expanded) {
        delete article.dataset.xreMediaExpanded;
      } else {
        article.dataset.xreMediaExpanded = "true";
      }

      updateMediaToggleText(article);
    }, true);
  }

  function updatePanel(root) {
    const fab = root.querySelector(".xre-fab");
    const panel = root.querySelector(".xre-panel");

    if (fab && panel) {
      panel.hidden = !settings.panelOpen;
      fab.setAttribute("aria-expanded", String(settings.panelOpen));
    }

    root.querySelectorAll("input[data-xre-key]").forEach((input) => {
      input.checked = Boolean(settings[input.dataset.xreKey]);
    });

    updatePanelPlacement(root);
  }

  function setupPanelDrag(root, handles, onDragEnd) {
    handles.filter(Boolean).forEach((handle) => {
      handle.addEventListener("pointerdown", (event) => {
        if (event.button !== 0) {
          return;
        }

        const startRect = root.getBoundingClientRect();
        const state = {
          handle,
          pointerId: event.pointerId,
          startX: event.clientX,
          startY: event.clientY,
          startLeft: startRect.left,
          startTop: startRect.top,
          moved: false,
        };

        const onPointerMove = (moveEvent) => {
          if (moveEvent.pointerId !== state.pointerId) {
            return;
          }

          const dx = moveEvent.clientX - state.startX;
          const dy = moveEvent.clientY - state.startY;

          if (Math.hypot(dx, dy) > DRAG_CLICK_TOLERANCE) {
            state.moved = true;
          }

          setPanelPosition(root, clampPanelPosition(root, state.startLeft + dx, state.startTop + dy));
        };

        const onPointerUp = (upEvent) => {
          if (upEvent.pointerId !== state.pointerId) {
            return;
          }

          document.removeEventListener("pointermove", onPointerMove);
          document.removeEventListener("pointerup", onPointerUp);
          document.removeEventListener("pointercancel", onPointerUp);
          root.classList.remove("xre-dragging");

          if (state.moved) {
            const rect = root.getBoundingClientRect();
            const position = clampPanelPosition(root, rect.left, rect.top);
            setPanelPosition(root, position);
            writeSetting("panelPosition", position);
            onDragEnd(state.handle);
          }
        };

        event.preventDefault();
        root.classList.add("xre-dragging");
        handle.setPointerCapture?.(event.pointerId);
        document.addEventListener("pointermove", onPointerMove);
        document.addEventListener("pointerup", onPointerUp);
        document.addEventListener("pointercancel", onPointerUp);
      });
    });

    window.addEventListener("resize", () => {
      const rootElement = document.getElementById("xre-root");

      if (!rootElement) {
        return;
      }

      if (isValidPosition(settings.panelPosition)) {
        const position = clampPanelPosition(rootElement, settings.panelPosition.left, settings.panelPosition.top);
        setPanelPosition(rootElement, position);
        writeSetting("panelPosition", position);
      }

      updatePanelPlacement(rootElement);
    });
  }

  function applyPanelPosition(root) {
    if (!isValidPosition(settings.panelPosition)) {
      updatePanelPlacement(root);
      return;
    }

    setPanelPosition(root, clampPanelPosition(root, settings.panelPosition.left, settings.panelPosition.top));
  }

  function isValidPosition(position) {
    return (
      position &&
      Number.isFinite(position.left) &&
      Number.isFinite(position.top)
    );
  }

  function clampPanelPosition(root, left, top) {
    const rect = root.getBoundingClientRect();
    const width = rect.width || 55;
    const height = rect.height || 55;
    const maxLeft = Math.max(DRAG_MARGIN, window.innerWidth - width - DRAG_MARGIN);
    const maxTop = Math.max(DRAG_MARGIN, window.innerHeight - height - DRAG_MARGIN);

    return {
      left: Math.round(Math.min(Math.max(DRAG_MARGIN, left), maxLeft)),
      top: Math.round(Math.min(Math.max(DRAG_MARGIN, top), maxTop)),
    };
  }

  function setPanelPosition(root, position) {
    root.classList.add("xre-positioned");
    root.style.left = `${position.left}px`;
    root.style.top = `${position.top}px`;
    root.style.right = "auto";
    root.style.bottom = "auto";
    updatePanelPlacement(root);
  }

  function updatePanelPlacement(root) {
    const panel = root.querySelector(".xre-panel");

    if (!panel) {
      return;
    }

    const rootRect = root.getBoundingClientRect();
    const panelWidth = panel.offsetWidth || 238;
    const panelHeight = panel.offsetHeight || 210;
    const needsRightExpand = rootRect.right < panelWidth + PANEL_OFFSET + DRAG_MARGIN;
    const needsBelow = rootRect.top < panelHeight + PANEL_OFFSET + DRAG_MARGIN;

    root.classList.toggle("xre-panel-right", needsRightExpand);
    root.classList.toggle("xre-panel-below", needsBelow);
  }

  function startObserver() {
    if (observer || !document.body) {
      return;
    }

    observer = new MutationObserver(queueScan);
    observer.observe(document.body, {
      childList: true,
      subtree: true,
    });
  }

  function queueScan() {
    if (scanQueued) {
      return;
    }

    scanQueued = true;
    window.requestAnimationFrame(() => {
      scanQueued = false;
      ensurePanel();
      scanPage();
    });
  }

  function scanPage() {
    applyRootClasses();

    const articles = document.querySelectorAll('article[data-testid="tweet"], article[role="article"]');
    articles.forEach((article) => {
      syncArticleMediaToggle(article);

      if (settings.hideAds && isPromotedArticle(article)) {
        article.dataset.xreAdHidden = "true";
      } else {
        delete article.dataset.xreAdHidden;
      }
    });
  }

  function syncArticleMediaToggle(article) {
    const hasMedia = Boolean(article.querySelector(MEDIA_SELECTORS));
    const button = article.querySelector(".xre-media-toggle");

    if (!hasMedia) {
      delete article.dataset.xreHasMedia;
      delete article.dataset.xreMediaExpanded;
      button?.remove();
      return;
    }

    article.dataset.xreHasMedia = "true";

    if (!button) {
      insertMediaToggle(article);
    } else {
      updateMediaToggleText(article);
    }
  }

  function insertMediaToggle(article) {
    const button = document.createElement("button");
    button.className = "xre-media-toggle";
    button.type = "button";
    updateMediaToggleText(article, button);

    const mediaBlock = findMediaBlock(article);

    if (mediaBlock?.parentElement) {
      mediaBlock.parentElement.insertBefore(button, mediaBlock);
      return;
    }

    article.appendChild(button);
  }

  function findMediaBlock(article) {
    const block = article.querySelector(MEDIA_BLOCK_SELECTORS);

    if (block) {
      return block;
    }

    const media = article.querySelector(MEDIA_SELECTORS);
    return media?.closest?.(MEDIA_BLOCK_SELECTORS) || media;
  }

  function updateMediaToggleText(article, button = article.querySelector(".xre-media-toggle")) {
    if (!button) {
      return;
    }

    button.textContent = article.dataset.xreMediaExpanded === "true"
      ? "隐藏图片/视频"
      : "显示图片/视频";
  }

  function isPromotedArticle(article) {
    if (article.querySelector('[data-testid="placementTracking"]')) {
      return true;
    }

    const labelNodes = article.querySelectorAll('span, div[dir="auto"], [aria-label]');

    for (const node of labelNodes) {
      if (node.closest('[data-testid="tweetText"]')) {
        continue;
      }

      if (isPromotedText(node.textContent) || isPromotedText(node.getAttribute("aria-label"))) {
        return true;
      }
    }

    return false;
  }

  function isPromotedText(value) {
    const text = normalizeText(value);

    if (!text || text.length > 64) {
      return false;
    }

    return AD_PATTERNS.some((pattern) => pattern.test(text));
  }

  function normalizeText(value) {
    return String(value || "")
      .replace(/\s+/g, " ")
      .replace(/[\u00a0\u200b-\u200d\ufeff]/g, "")
      .trim();
  }
})();