Hide MAL-Sync Chapter Popups

Hides MAL-Sync popups that contain "Chapter: X/X" so they don't appear after finishing a chapter.

当前为 2025-09-25 提交的版本,查看 最新版本

// ==UserScript==
// @name         Hide MAL-Sync Chapter Popups
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  Hides MAL-Sync popups that contain "Chapter: X/X" so they don't appear after finishing a chapter.
// @author       YourName
// @match        (Insert your urls here)
// @run-at       document-end
// @grant        none
// @license MIT
// ==/UserScript==

(function () {
  const CHAPTER_PATTERN = /Chapter\s*[::]\s*\d+(?:\s*[\//]\s*(?:\d+|[\??]))?/i;
  const FLASH_SEL = '.flash.type-update.flashinfo';
  const SENTINEL = 'data-hidden-by-hide-malsync';

  // Add CSS so we "win" even if inline styles are toggled later.
  const style = document.createElement('style');
  style.textContent = `[${SENTINEL}="1"]{display:none!important;visibility:hidden!important;pointer-events:none!important}`;
  (document.head || document.documentElement).appendChild(style);

  const normalize = (s) =>
    (s || '')
      .replace(/\u00A0/g, ' ') // nbsp -> space
      .replace(/\s+/g, ' ') // collapse whitespace
      .trim();

  function shouldHide(el) {
    const txt = normalize(el.textContent);
    return CHAPTER_PATTERN.test(txt);
  }

  function hide(el) {
    if (el.getAttribute(SENTINEL) === '1') return;
    el.setAttribute(SENTINEL, '1');
    // belt-and-suspenders inline styles
    el.style.display = 'none';
    el.style.visibility = 'hidden';
    el.style.pointerEvents = 'none';
  }

  function sweep(root = document) {
    root.querySelectorAll(FLASH_SEL).forEach((el) => {
      if (shouldHide(el)) hide(el);
    });
  }

  function observe(root) {
    const mo = new MutationObserver((muts) => {
      for (const m of muts) {
        if (m.type === 'childList') {
          m.addedNodes.forEach((n) => {
            if (n.nodeType !== 1) return;
            if (n.matches?.(FLASH_SEL)) {
              if (shouldHide(n)) hide(n);
            } else {
              n.querySelectorAll?.(FLASH_SEL).forEach((el) => {
                if (shouldHide(el)) hide(el);
              });
            }
          });
        } else if (m.type === 'characterData') {
          const el = m.target.parentElement?.closest?.(FLASH_SEL);
          if (el && shouldHide(el)) hide(el);
        } else if (m.type === 'attributes') {
          const t = m.target;
          if (t instanceof Element && t.matches(FLASH_SEL) && shouldHide(t)) hide(t);
        }
      }
    });
    mo.observe(root, {
      subtree: true,
      childList: true,
      characterData: true,
      attributes: true,
      attributeFilter: ['class', 'style'],
    });
  }

  // If your reader injects into iframes (same-origin), cover those too:
  function watchIframe(iframe) {
    const hook = () => {
      try {
        const d = iframe.contentDocument;
        if (!d) return;
        sweep(d);
        observe(d);
      } catch {}
    };
    iframe.addEventListener('load', hook);
    hook();
  }

  const start = () => {
    sweep(document);
    observe(document);
    document.querySelectorAll('iframe').forEach(watchIframe);
    // Safety net for odd race conditions
    setInterval(() => sweep(document), 1500);
  };

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', start, { once: true });
  } else {
    start();
  }
})();