// ==UserScript== // @name kone 썸네일 + 댓글 개선 // @namespace http://tampermonkey.net/ // @version 3.1 // @description 마우스 오버시 썸네일 표시 + 댓글 자동 확장 + 스크롤 제거 // @author 김머시기 // @match https://kone.gg/* // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @license MIT // @run-at document-idle // @downloadURL https://update.greasyfork.icu/scripts/536425/kone%20%EC%8D%B8%EB%84%A4%EC%9D%BC%20%2B%20%EB%8C%93%EA%B8%80%20%EA%B0%9C%EC%84%A0.user.js // @updateURL https://update.greasyfork.icu/scripts/536425/kone%20%EC%8D%B8%EB%84%A4%EC%9D%BC%20%2B%20%EB%8C%93%EA%B8%80%20%EA%B0%9C%EC%84%A0.meta.js // ==/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 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 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 showPreviewAtMouse(event, url, thisHoverId) { document.addEventListener('mousemove', moveHandler); document.addEventListener('keydown', onKeyDown); moveHandler(event); previewBox.style.display = 'block'; iframe.onload = () => { if (thisHoverId !== hoverId) return; 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); 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 }); } } const styleFix = document.createElement('style'); styleFix.textContent = ` .comment-wrapper, .overflow-x-auto, .overflow-hidden { overflow: visible !important; max-height: none !important; } `; document.head.appendChild(styleFix); function observeURLChange() { let lastUrl = location.href; const observer = new MutationObserver(() => { if (location.href !== lastUrl && location.href.includes('/s/')) { lastUrl = location.href; setTimeout(() => { runCommentFix(); attachEvents(); }, 1000); } }); observer.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(); }, 1000); } }; window.addEventListener('popstate', () => { if (location.href.includes('/s/')) { setTimeout(() => { runCommentFix(); attachEvents(); }, 1000); } }); } setTimeout(() => { runCommentFix(); observeURLChange(); }, 1500); })();