// ==UserScript==
// @name kone 썸네일
// @namespace http://tampermonkey.net/
// @version 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
// ==/UserScript==
(async function () {
'use strict';
let hoverDelay = await GM_getValue('hoverDelay', 300);
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 = [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, title: `자동 슬라이드 ${autoSlide === false ? '꺼짐' : `${(autoSlide / 1000).toFixed(1)}초`}` }
);
if (MenuID[1]) GM_unregisterMenuCommand(MenuID[1]);
MenuID[1] = GM_registerMenuCommand(
`썸네일 크기 : ${thumbSize}px`,
toggleThumbSize,
{ autoClose: false, title: `썸네일 크기 ${thumbSize}px` }
);
if (MenuID[2]) GM_unregisterMenuCommand(MenuID[2]);
MenuID[2] = GM_registerMenuCommand(
`마우스 오버 : ${hoverDelay}ms`,
toggleHoverDelay,
{ autoClose: false, title: `마우스 오버 ${hoverDelay}ms` }
);
}
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 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', () => {
previewBox.style.display = 'none';
imageList = [];
isPreviewVisible = false;
clearTimeout(hoverTimer);
stopAutoSlide();
currentHoverTarget = null;
iframe.src = 'about:blank';
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 anchors = document.querySelectorAll('a[href^="/s/"]:not([data-preview-init])');
anchors.forEach(link => {
link.dataset.previewInit = '1';
link.addEventListener('mouseenter', e => handleMouseEnter(e, link, link.getAttribute('href')));
link.addEventListener('mouseleave', () => {
clearTimeout(hoverTimer);
currentHoverTarget = null;
});
});
const groupLinks = document.querySelectorAll('a.group[href*="/s/"]:not([data-preview-init])');
groupLinks.forEach(link => {
link.dataset.previewInit = '1';
link.addEventListener('mouseenter', e => handleMouseEnter(e, link, link.getAttribute('href')));
link.addEventListener('mouseleave', () => {
clearTimeout(hoverTimer);
currentHoverTarget = null;
});
});
}
const observer = new MutationObserver(attachEvents);
observer.observe(document.body, { childList: true, subtree: true });
attachEvents();
})();