// ==UserScript== // @name kone 썸네일 // @namespace http://tampermonkey.net/ // @version 2.2.1 // @author 김머시기 // @description 마우스 오버시 썸네일 표시, 좌우 방향키로 넘기기 가능 // @match https://kone.gg/* // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @license MIT // @run-at document-idle // @downloadURL none // ==/UserScript== (async function () { 'use strict'; let hoverDelay = await GM_getValue('hoverDelay', 100); let thumbSize = await GM_getValue('thumbSize', 400); let autoSlide = await GM_getValue('autoSlide', false); const MenuID = [null, null, 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(); } async function toggleHoverDelay() { const delays = [100, 300, 500, 700, 1000]; let idx = delays.indexOf(hoverDelay); hoverDelay = delays[(idx + 1) % delays.length]; await GM_setValue('hoverDelay', hoverDelay); updateMenu(); } function updateMenu() { if (MenuID[0]) GM_unregisterMenuCommand(MenuID[0]); MenuID[0] = GM_registerMenuCommand( `자동 슬라이드 : ${autoSlide === false ? '꺼짐' : `${(autoSlide / 1000).toFixed(1)}초`}`, toggleAutoSlide, { autoClose: false } ); if (MenuID[1]) GM_unregisterMenuCommand(MenuID[1]); MenuID[1] = GM_registerMenuCommand( `썸네일 크기  : ${thumbSize}px`, toggleThumbSize, { autoClose: false } ); if (MenuID[2]) GM_unregisterMenuCommand(MenuID[2]); MenuID[2] = GM_registerMenuCommand( `마우스 오버  : ${hoverDelay}ms`, toggleHoverDelay, { 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') { 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 hidePreview() { previewBox.style.display = 'none'; imageList = []; isPreviewVisible = false; stopAutoSlide(); 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(); } 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(); })();