DCInside 자짤 차단기

자짤 숨기기

// ==UserScript==
// @name         DCInside 자짤 차단기
// @namespace    http://tampermonkey.net/
// @version      0.5
// @description  자짤 숨기기
// @match        *://gall.dcinside.com/*
// @grant        none
// @license MIT
// @run-at       document-start
// ==/UserScript==

(function () {
  'use strict';

  // ----- 스타일 (동일) -----
  const style = document.createElement('style');
  style.textContent = `
    img[src*="viewimage.php"] { display: none !important; }
    img[srcset*="viewimage.php"] { display: none !important; }
    img[data-src*="viewimage.php"] { display: none !important; }
    picture > source[srcset*="viewimage.php"] ~ img { display: none !important; }

    img[src*="viewimage.php"][data-fileno],
    img[srcset*="viewimage.php"][data-fileno],
    img[data-src*="viewimage.php"][data-fileno],
    img[src*="viewimage.php"][data-tempno],
    img[srcset*="viewimage.php"][data-tempno],
    img[data-src*="viewimage.php"][data-tempno],
    img[src*="viewimage.php"][onclick*="imgPop"],
    img[srcset*="viewimage.php"][onclick*="imgPop"],
    img[data-src*="viewimage.php"][onclick*="imgPop"],
    img[src*="viewimage.php"][data-original],
    img[srcset*="viewimage.php"][data-original],
    img[data-src*="viewimage.php"][data-original] { display: block !important; }

    img.dcij-show { display: block !important; }

    .dcij-wrap { position: relative; display: inline-block; max-width: 100%; }

    .dcij-toggle {
      padding: 4px 8px;
      font-size: 12px;
      line-height: 1;
      border: 1px solid rgba(0,0,0,.15);
      background: rgba(255,255,255,.95);
      border-radius: 6px;
      cursor: pointer;
      user-select: none;
      box-shadow: 0 1px 3px rgba(0,0,0,.08);
      transition: opacity .2s;
    }
    .dcij-toggle[data-state="hidden"]::after { content: "보기"; }
    .dcij-toggle[data-state="visible"]::after { content: "숨기기"; }

    /* 숨김: 버튼은 흐름(static), 항상 보임 */
    .dcij-wrap[data-state="hidden"] .dcij-toggle {
      position: static;
      display: inline-block;
      margin: 4px 0 8px 0;
      opacity: 1;
    }
    /* 보기: 버튼은 이미지 내부 좌상단, 호버시에만 보임 */
    .dcij-wrap[data-state="visible"] .dcij-toggle {
      position: absolute;
      top: 8px; left: 8px;
      margin: 0;
      z-index: 2147483647;
      opacity: 0;
    }
    .dcij-wrap[data-state="visible"]:hover .dcij-toggle { opacity: 1; }
  `;
  (document.head || document.documentElement).insertBefore(
    style, (document.head || document.documentElement).firstChild
  );

  const BLOCK_IMG = [
    'img[src*="viewimage.php"]',
    'img[srcset*="viewimage.php"]',
    'img[data-src*="viewimage.php"]',
    'picture > source[srcset*="viewimage.php"] ~ img'
  ].join(',');
  const EXEMPT_IMG = 'img[data-fileno], img[data-tempno], img[onclick*="imgPop"], img[data-original]';
  const isExempt = (img) => img.matches(EXEMPT_IMG);

  // ====== 루프 방지용 가드 ======
  let MUTATING = 0;
  const guard = (fn) => (...args) => { MUTATING++; try { return fn(...args); } finally { MUTATING--; } };

  function ensureWrap(node) {
    let wrap = node.closest?.('.dcij-wrap');
    if (wrap) return wrap;
    const parent = node.parentNode;
    if (!parent) return null;
    wrap = document.createElement('span');
    wrap.className = 'dcij-wrap';
    wrap.setAttribute('data-state', 'hidden');
    parent.insertBefore(wrap, node);
    wrap.appendChild(node);
    return wrap;
  }

  const unhideAncestorsFor = guard(function (wrap, img, turnOn) {
    let el = img;
    while (el && el !== wrap.parentElement && el !== document.documentElement) {
      const cs = window.getComputedStyle(el);
      if (turnOn) {
        if (cs.display === 'none' && el.dataset.dcijUnhid !== '1') {
          el.dataset.dcijPrevDisplay = el.style.display || '';
          el.dataset.dcijUnhid = '1';
          el.style.display = 'block';
        }
      } else {
        if (el.dataset.dcijUnhid === '1') {
          el.style.display = el.dataset.dcijPrevDisplay || '';
          delete el.dataset.dcijPrevDisplay;
          delete el.dataset.dcijUnhid;
        }
      }
      el = el.parentElement;
    }
  });

  const toggleWrap = guard(function (wrap, toShow) {
    const imgs = wrap.querySelectorAll('img');
    if (toShow) {
      imgs.forEach(img => {
        if (img.loading === 'lazy') img.loading = 'eager';
        if (img.dataset?.src && !img.src) img.src = img.dataset.src;
        img.classList.add('dcij-show');
        unhideAncestorsFor(wrap, img, true);
      });
      wrap.setAttribute('data-state', 'visible');
      wrap.querySelector('.dcij-toggle')?.setAttribute('data-state', 'visible');
    } else {
      imgs.forEach(img => {
        img.classList.remove('dcij-show');
        unhideAncestorsFor(wrap, img, false);
      });
      wrap.setAttribute('data-state', 'hidden');
      wrap.querySelector('.dcij-toggle')?.setAttribute('data-state', 'hidden');
    }
  });

  function addToggleButton(img) {
    if (img.dataset.dcijBtn === '1') return;
    const wrap = ensureWrap(img);
    if (!wrap) return;

    const btn = document.createElement('button');
    btn.type = 'button';
    btn.className = 'dcij-toggle';
    btn.setAttribute('aria-label', '가린 이미지 보기/숨기기');

    const hiddenNow = !img.classList.contains('dcij-show');
    btn.setAttribute('data-state', hiddenNow ? 'hidden' : 'visible');
    wrap.setAttribute('data-state', hiddenNow ? 'hidden' : 'visible');

    btn.addEventListener('click', (e) => {
      e.stopPropagation();
      const toShow = btn.getAttribute('data-state') === 'hidden';
      toggleWrap(wrap, toShow);
    });

    guard(() => wrap.appendChild(btn))();
    img.dataset.dcijBtn = '1';
  }

  const tagHidden = guard(function (img) {
    img.classList.add('dcij-hidden');
  });

  function processImg(img) {
    if (!img || img.nodeName !== 'IMG') return;
    if (isExempt(img)) {
      guard(() => {
        img.classList.remove('dcij-hidden', 'dcij-show');
        const w = ensureWrap(img);
        if (w) toggleWrap(w, true);
      })();
      return;
    }
    tagHidden(img);
    addToggleButton(img);
  }

  function scan(root = document) {
    root.querySelectorAll(BLOCK_IMG).forEach(processImg);
  }

  // ====== 옵저버 (style/class 제외) ======
  const mo = new MutationObserver((muts) => {
    if (MUTATING) return; // 우리가 수정 중이면 무시
    for (const m of muts) {
      if (m.type === 'childList') {
        m.addedNodes.forEach((n) => {
          if (n.nodeType !== 1) return;
          if (n.matches?.(BLOCK_IMG)) processImg(n);
          else scan(n);
        });
      } else if (m.type === 'attributes') {
        const el = m.target;
        if (!el.matches) continue;
        if (el.matches(BLOCK_IMG)) processImg(el);
        if (el.matches('img')) {
          if (isExempt(el)) {
            guard(() => {
              el.classList.remove('dcij-hidden', 'dcij-show');
              const w = ensureWrap(el);
              if (w) toggleWrap(w, true);
            })();
          } else if (
            el.src?.includes('viewimage.php') ||
            el.getAttribute('srcset')?.includes('viewimage.php') ||
            el.getAttribute('data-src')?.includes('viewimage.php')
          ) {
            tagHidden(el);
            addToggleButton(el);
          }
        }
      }
    }
  });
  mo.observe(document.documentElement, {
    childList: true,
    subtree: true,
    // ★ style/class 제거: 무한 루프 방지
    attributes: true,
    attributeFilter: ['src','srcset','data-src','data-fileno','data-tempno','onclick','data-original']
  });

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