kone 개선

마우스 오버시 썸네일 표시 + 댓글 자동 확장 + 다크모드 개선

目前為 2025-05-23 提交的版本,檢視 最新版本

// ==UserScript==
// @name          kone 개선
// @namespace     http://tampermonkey.net/
// @version       4.5
// @description   마우스 오버시 썸네일 표시 + 댓글 자동 확장 + 다크모드 개선
// @author        김머시기
// @match         https://kone.gg/*
// @grant         GM_getValue
// @grant         GM_setValue
// @grant         GM_registerMenuCommand
// @grant         GM_unregisterMenuCommand
// @license       MIT
// @run-at        document-idle
// ==/UserScript==

(async function () {
  'use strict';

  let thumbSize = await GM_getValue('thumbSize', 400);
  let autoSlide = await GM_getValue('autoSlide', false);
  const MenuID = [null, null];
  const initializedLinks = new WeakSet();

  function updateMenu() {
    if (MenuID[1]) GM_unregisterMenuCommand(MenuID[1]);
    MenuID[1] = GM_registerMenuCommand(
      `자동 슬라이드 : ${autoSlide === false ? '꺼짐' : `${(autoSlide / 1000).toFixed(1)}초`}`,
      async () => {
        const states = [false, 1500, 2500, 3500];
        let idx = states.indexOf(autoSlide);
        autoSlide = states[(idx + 1) % states.length];
        await GM_setValue('autoSlide', autoSlide);
        updateMenu();
      },
      { autoClose: false }
    );

    if (MenuID[0]) GM_unregisterMenuCommand(MenuID[0]);
    MenuID[0] = GM_registerMenuCommand(
      `썸네일 크기  : ${thumbSize}px`,
      async () => {
        const sizes = [200, 320, 400, 480, 720];
        let idx = sizes.indexOf(thumbSize);
        thumbSize = sizes[(idx + 1) % sizes.length];
        await GM_setValue('thumbSize', thumbSize);
        updateMenu();
      },
      { autoClose: false }
    );
  }

  updateMenu();

  const previewBox = document.createElement('div');
  const previewImage = document.createElement('img');
  const iframe = document.createElement('iframe');
  let hoverId = 0;
  let currentIndex = 0;
  let imageList = [];
  let isPreviewVisible = false;
  let hoverTimer = null;
  let autoSlideTimer = null;

  Object.assign(previewBox.style, {
    position: 'fixed',
    pointerEvents: 'none',
    zIndex: 9999,
    display: 'none',
    border: '1px solid #ccc',
    background: '#fff',
    padding: '4px',
    boxShadow: '0 0 8px rgba(0,0,0,0.3)',
    borderRadius: '6px'
  });

  Object.assign(previewImage.style, {
    width: '100%',
    height: 'auto',
    objectFit: 'contain',
    display: 'block'
  });

  Object.assign(iframe.style, {
    position: 'fixed',
    left: '-9999px',
    width: '1px',
    height: '1px',
    visibility: 'hidden'
  });

  previewBox.appendChild(previewImage);
  document.body.appendChild(previewBox);
  document.body.appendChild(iframe);

  function applySize() {
    previewBox.style.maxWidth = thumbSize + 'px';
    previewBox.style.maxHeight = thumbSize + 'px';
    previewImage.style.maxWidth = thumbSize + 'px';
    previewImage.style.maxHeight = thumbSize + 'px';
  }

  function updateImage() {
    if (imageList.length > 0) {
      previewImage.src = imageList[currentIndex];
      previewBox.style.display = 'block';
    } else {
      hidePreview();
    }
  }

  function startAutoSlide() {
    if (autoSlideTimer) clearInterval(autoSlideTimer);
    if (typeof autoSlide === 'number' && imageList.length > 1) {
      autoSlideTimer = setInterval(() => {
        currentIndex = (currentIndex + 1) % imageList.length;
        updateImage();
      }, autoSlide);
    }
  }

  function stopAutoSlide() {
    clearInterval(autoSlideTimer);
    autoSlideTimer = null;
  }

  function onKeyDown(e) {
    if (!isPreviewVisible) return;
    if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {
      e.preventDefault();
      if (e.key === 'ArrowRight') {
        currentIndex = (currentIndex + 1) % imageList.length;
      } else {
        currentIndex = (currentIndex - 1 + imageList.length) % imageList.length;
      }
      updateImage();
    }
  }

  function extractImagesFromIframeDocument(doc) {
    const proseContainer = doc.querySelector('div.prose-container');
    if (!proseContainer || !proseContainer.shadowRoot) {
      return [];
    }
    const contentInShadow = proseContainer.shadowRoot.querySelector('div.content');
    if (!contentInShadow) {
      return [];
    }
    return [...contentInShadow.querySelectorAll('img')]
      .map(img => img.src)
      .filter(src => (
        src && !/kone-logo|default|placeholder|data:image/.test(src)
      ));
  }

  function moveHandler(e) {
    const padding = 20;
    const boxW = previewBox.offsetWidth || thumbSize;
    const boxH = previewBox.offsetHeight || thumbSize;

    let left = e.clientX + padding;
    let top = e.clientY + padding;

    if (left + boxW > window.innerWidth) left = e.clientX - boxW - padding;
    if (top + boxH > window.innerHeight) top = e.clientY - boxH - padding;

    previewBox.style.left = `${Math.max(0, left)}px`;
    previewBox.style.top = `${Math.max(0, top)}px`;
  }

  function hidePreview() {
    previewBox.style.display = 'none';
    previewImage.src = '';
    iframe.src = 'about:blank';
    imageList = [];
    isPreviewVisible = false;
    stopAutoSlide();
    document.removeEventListener('mousemove', moveHandler);
    document.removeEventListener('keydown', onKeyDown);
  }

  function hideElementInIframe(doc, selector) {
    try {
        const elements = doc.querySelectorAll(selector);
        elements.forEach(el => {
            if (el.offsetParent !== null) {
                el.style.setProperty('display', 'none', 'important');
            }
        });
    } catch (e) { }
  }

  async function handleModalsInIframeKone(doc) {
    try {
        const nsfwOverlayContainer = doc.querySelector('div.relative.min-h-60 > div.absolute.w-full.h-full.backdrop-blur-2xl');
        if (nsfwOverlayContainer && nsfwOverlayContainer.offsetParent !== null) {
            const viewContentButton = nsfwOverlayContainer.querySelector('div.flex.gap-4 button:nth-child(2)');
            if (viewContentButton && viewContentButton.textContent?.includes('콘텐츠 보기')) {
                viewContentButton.click();
                await new Promise(resolve => setTimeout(resolve, 500));
            } else {
                hideElementInIframe(doc, '.age-verification-popup');
                hideElementInIframe(doc, '.content-overlay.block');
            }
        } else {
            hideElementInIframe(doc, '.age-verification-popup');
            hideElementInIframe(doc, '.content-overlay.block');
        }
    } catch (e) { }
  }

  function showPreviewAtMouse(event, url, thisHoverId) {
    document.addEventListener('mousemove', moveHandler);
    document.addEventListener('keydown', onKeyDown);

    moveHandler(event);

    iframe.onload = async () => {
      if (thisHoverId !== hoverId) return;
      try {
        const doc = iframe.contentDocument || iframe.contentWindow.document;
        if (doc) {
            await handleModalsInIframeKone(doc);
            imageList = extractImagesFromIframeDocument(doc);
            currentIndex = 0;
            applySize();
            updateImage();
            isPreviewVisible = true;
            if (imageList.length > 0) {
                    previewBox.style.display = 'block';
            } else {
                    hidePreview();
            }
            startAutoSlide();
        } else {
            hidePreview();
        }
      } catch (e) {
        hidePreview();
      }
    };
    iframe.src = url;
  }

  function handleMouseEnter(event, element, href) {
    clearTimeout(hoverTimer);
    const thisHoverId = ++hoverId;

    hoverTimer = setTimeout(() => {
      if (thisHoverId !== hoverId) return;
      const fullUrl = href.startsWith('http') ? href : location.origin + href;
      showPreviewAtMouse(event, fullUrl, thisHoverId);
    }, 100);
  }

  function attachEvents() {
    const allLinks = document.querySelectorAll('a[href*="/s/"]');
    allLinks.forEach(link => {
      if (initializedLinks.has(link)) return;
      initializedLinks.add(link);

      link.addEventListener('mouseenter', e => handleMouseEnter(e, link, link.getAttribute('href')));
      link.addEventListener('mouseleave', () => {
        clearTimeout(hoverTimer);
        hidePreview();
      });
      link.addEventListener('click', hidePreview);
    });
  }

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

  function clickAllExpandButtons() {
    const expandButtons = document.querySelectorAll('button.group.pointer-events-auto');
    let clickedAny = false;

    expandButtons.forEach(button => {
      try {
        const hasPlusIcon = button.querySelector('.lucide-circle-plus');
        if (hasPlusIcon) {
          button.click();
          clickedAny = true;
        }
      } catch (e) {}
    });

    if (clickedAny) {
      setTimeout(clickAllExpandButtons, 500);
    }
  }

  function runCommentFix(retry = 0) {
    const commentEl = document.querySelector('#comment');
    if (!commentEl) {
      if (retry < 10) {
        setTimeout(() => runCommentFix(retry + 1), 500);
      }
      return;
    }

    clickAllExpandButtons();

    const container = commentEl.parentElement;
    if (container) {
      const observer = new MutationObserver(() => {
        clickAllExpandButtons();
      });
      observer.observe(container, { childList: true, subtree: true });
    }
  }

  // 기존 styleFix와 다크 모드 스타일을 통합하여 관리
  const styleFix = document.createElement('style');
  document.head.appendChild(styleFix);

  // 다크 모드 스타일을 업데이트하는 함수
  function updateGlobalStyles() {
    const isDarkMode = document.documentElement.classList.contains('dark');
    let css = `
      /* 댓글 영역의 overflow 및 max-height만 명확히 제어 */
      .comment-wrapper,
      .comment-wrapper .overflow-x-auto,
      .comment-wrapper .overflow-hidden,
      .thread-body-content .overflow-hidden { /* 댓글 내용 자체의 overflow-hidden도 포함 */
        overflow: visible !important;
        max-height: none !important;
      }
    `;

    if (isDarkMode) {
      css += `
        /* 본문 (Shadow DOM 외부, 혹은 영향을 받는 일반 텍스트) */
        html.dark .prose-container:not([style*="display: none"]) p[style*="color: rgb(24, 24, 24)"],
        html.dark .prose-container:not([style*="display: none"]) p[style*="color: #181818"],
        html.dark .prose-container:not([style*="display: none"]) span[style*="color: rgb(24, 24, 24)"],
        html.dark .prose-container:not([style*="display: none"]) span[style*="color: #181818"] {
          color: white !important;
        }
        html.dark .prose-container:not([style*="display: none"]) p:not([style]),
        html.dark .prose-container:not([style*="display: none"]) span:not([style]) {
            color: white !important;
        }
        html.dark .prose-container:not([style*="display: none"]) a {
            color: #88c0d0 !important;
        }

        /* 댓글 영역 */
        html.dark div.thread-body-content p[style*="color: rgb(24, 24, 24)"],
        html.dark div.thread-body-content p[style*="color: #181818"],
        html.dark div.thread-body-content span[style*="color: rgb(24, 24, 24)"],
        html.dark div.thread-body-content span[style*="color: #181818"],
        html.dark p.text-sm.max-w-xl.whitespace-pre-wrap[style*="color: rgb(24, 24, 24)"],
        html.dark p.text-sm.max-w-xl.whitespace-pre-wrap[style*="color: #181818"],
        html.dark p.text-sm.max-w-xl.whitespace-pre-wrap:not([style]) {
            color: white !important;
        }
        html.dark div.thread-body-content p:not([style]),
        html.dark div.thread-body-content span:not([style]) {
            color: white !important;
        }
        html.dark div.thread-body-content a,
        html.dark p.text-sm.max-w-xl.whitespace-pre-wrap a {
            color: #88c0d0 !important;
        }
      `;
    }
    styleFix.textContent = css;
  }

  // Shadow DOM 내부 스타일 업데이트 함수 (Shadow DOM이 로드될 때만 필요)
  function addDarkmodeStyleToShadowDOMInternal() {
    const proseContainer = document.querySelector('div.prose-container');
    const isDarkMode = document.documentElement.classList.contains('dark');

    if (proseContainer && proseContainer.shadowRoot) {
      let shadowStyle = proseContainer.shadowRoot.querySelector('#somi-dark-mode-style');
      if (!shadowStyle) {
        shadowStyle = document.createElement('style');
        shadowStyle.id = 'somi-dark-mode-style';
        proseContainer.shadowRoot.appendChild(shadowStyle);
      }

      if (isDarkMode) {
        shadowStyle.textContent = `
          .content p[style*="color: rgb(24, 24, 24)"],
          .content p[style*="color: #181818"],
          .content span[style*="color: rgb(24, 24, 24)"],
          .content span[style*="color: #181818"] {
            color: white !important;
          }
          .content p:not([style]),
          .content span:not([style]) {
              color: white !important;
          }
          .content a {
              color: #88c0d0 !important;
          }
        `;
      } else {
        shadowStyle.textContent = '';
      }
    }
  }


  // HTML `dark` 클래스 변경 및 `prose-container` 생성 감지 옵저버
  const mainObserver = new MutationObserver((mutationsList) => {
    // 1. HTML의 class 속성 변경 감지 (다크 모드 토글)
    if (mutationsList.some(m => m.target === document.documentElement && m.attributeName === 'class')) {
      updateGlobalStyles(); // 전역 스타일 (댓글 포함) 업데이트
    }

    // 2. `prose-container`가 나타나고 shadowRoot가 열릴 때 처리 (본문)
    const proseContainer = document.querySelector('div.prose-container');
    if (proseContainer && proseContainer.shadowRoot) {
        addDarkmodeStyleToShadowDOMInternal();
    }
  });

  mainObserver.observe(document.body, { childList: true, subtree: true });
  mainObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });

  function observeURLChange() {
    let lastUrl = location.href;

    const urlChangeObserver = new MutationObserver(() => {
      if (location.href !== lastUrl && location.href.includes('/s/')) {
        lastUrl = location.href;
        setTimeout(() => {
          runCommentFix();
          attachEvents();
          updateGlobalStyles();
          addDarkmodeStyleToShadowDOMInternal();
        }, 500);
      }
    });

    urlChangeObserver.observe(document.body, { childList: true, subtree: true });

    const originalPush = history.pushState;
    history.pushState = function () {
      originalPush.apply(this, arguments);
      if (location.href.includes('/s/')) {
        setTimeout(() => {
          runCommentFix();
          attachEvents();
          updateGlobalStyles();
          addDarkmodeStyleToShadowDOMInternal();
        }, 500);
      }
    };

    window.addEventListener('popstate', () => {
      if (location.href.includes('/s/')) {
        setTimeout(() => {
          runCommentFix();
          attachEvents();
          updateGlobalStyles();
          addDarkmodeStyleToShadowDOMInternal();
        }, 500);
      }
    });
  }

  const initScript = () => {
    runCommentFix();
    observeURLChange();
    updateGlobalStyles();
    addDarkmodeStyleToShadowDOMInternal();
  };

  if (document.readyState === 'complete' || document.readyState === 'interactive') {
      initScript();
  } else {
      document.addEventListener('DOMContentLoaded', initScript);
  }
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址