Greasy Fork

kone 썸네일

마우스 오버시 썸네일 표시, 좌우 방향키로 넘기기 가능

当前为 2025-05-18 提交的版本,查看 最新版本

// ==UserScript==
// @name         kone 썸네일
// @namespace    http://tampermonkey.net/
// @version      2.3
// @author       김머시기
// @description  마우스 오버시 썸네일 표시, 좌우 방향키로 넘기기 가능
// @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';

  const hoverDelay = 100;
  let thumbSize = await GM_getValue('thumbSize', 400);
  let autoSlide = await GM_getValue('autoSlide', false);
  const MenuID = [null];

  async function toggleAutoSlide() {
    const states = [false, 1500, 2500, 3500];
    let idx = states.indexOf(autoSlide);
    autoSlide = states[(idx + 1) % states.length];
    await GM_setValue('autoSlide', autoSlide);
    updateMenu();
  }

  async function toggleThumbSize() {
    const sizes = [200, 320, 400, 480];
    let idx = sizes.indexOf(thumbSize);
    thumbSize = sizes[(idx + 1) % sizes.length];
    await GM_setValue('thumbSize', thumbSize);
    updateMenu();
  }

  function updateMenu() {
    if (MenuID[0]) GM_unregisterMenuCommand(MenuID[0]);
    MenuID[0] = GM_registerMenuCommand(
      `썸네일 크기  : ${thumbSize}px`,
      toggleThumbSize,
      { autoClose: false }
    );
  }

  updateMenu();

  let previewBox = document.createElement('div');
  let previewImage = document.createElement('img');
  let iframe = document.createElement('iframe');
  let currentIndex = 0;
  let imageList = [];
  let isPreviewVisible = false;
  let currentHoverTarget = null;
  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'
  });

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

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

  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 {
      previewBox.style.display = 'none';
    }
  }

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

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

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

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

  function startUrlWatch() {
    const _push = history.pushState;
    const _replace = history.replaceState;

    history.pushState = function (...args) {
      _push.apply(this, args);
      hidePreview();
    };
    history.replaceState = function (...args) {
      _replace.apply(this, args);
      hidePreview();
    };
    window.addEventListener('popstate', hidePreview);

    window.__restoreHistoryPatch = () => {
      history.pushState = _push;
      history.replaceState = _replace;
      window.removeEventListener('popstate', hidePreview);
    };
  }

  function stopUrlWatch() {
    if (window.__restoreHistoryPatch) {
      window.__restoreHistoryPatch();
      window.__restoreHistoryPatch = null;
    }
  }

  function hidePreview() {
    previewBox.style.display = 'none';
    imageList = [];
    isPreviewVisible = false;
    stopAutoSlide();
    stopUrlWatch();
    iframe.src = 'about:blank';
  }

  function showPreviewAtMouse(event, url) {
    previewBox.style.top = (event.clientY + 20) + 'px';
    previewBox.style.left = (event.clientX + 20) + 'px';

    iframe.onload = () => {
      try {
        const doc = iframe.contentDocument || iframe.contentWindow.document;
        imageList = extractImagesFromIframeDocument(doc);
        currentIndex = 0;
        applySize();
        updateImage();
        isPreviewVisible = true;
        startAutoSlide();
        startUrlWatch(); // 썸네일 열릴 때만 URL 감시
      } catch (e) {
        console.error('iframe access error', e);
      }
    };

    iframe.src = url;

    const moveHandler = e => {
      previewBox.style.top = (e.clientY + 20) + 'px';
      previewBox.style.left = (e.clientX + 20) + 'px';
    };

    document.addEventListener('mousemove', moveHandler);
    document.addEventListener('keydown', onKeyDown);

    event.target.addEventListener('mouseleave', () => {
      hidePreview();
      document.removeEventListener('mousemove', moveHandler);
      document.removeEventListener('keydown', onKeyDown);
    }, { once: true });
  }

  function handleMouseEnter(event, element, href) {
    clearTimeout(hoverTimer);
    currentHoverTarget = element;
    hoverTimer = setTimeout(() => {
      if (currentHoverTarget === element) {
        const fullUrl = href.startsWith('http') ? href : location.origin + href;
        showPreviewAtMouse(event, fullUrl);
      }
    }, hoverDelay);
  }

  function attachEvents() {
    const allLinks = document.querySelectorAll('a[href*="/s/"]:not([data-preview-init])');
    allLinks.forEach(link => {
      link.dataset.previewInit = '1';
      link.addEventListener('mouseenter', e => handleMouseEnter(e, link, link.getAttribute('href')));
      link.addEventListener('mouseleave', () => {
        clearTimeout(hoverTimer);
        currentHoverTarget = null;
      });
      link.addEventListener('click', () => {
        hidePreview();
      });
    });
  }

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