// ==UserScript==
// @name kone 썸네일 + 댓글 개선
// @namespace http://tampermonkey.net/
// @version 5.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
// ==/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');
let currentIframe = null;
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'
});
previewBox.appendChild(previewImage);
document.body.appendChild(previewBox);
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();
e.stopImmediatePropagation();
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() {
if (!isPreviewVisible && previewBox.style.display === 'none' && !currentIframe) {
return;
}
previewBox.style.display = 'none';
previewImage.src = '';
if (currentIframe) {
currentIframe.src = 'about:blank';
currentIframe.remove();
currentIframe = null;
}
imageList = [];
isPreviewVisible = false;
stopAutoSlide();
document.removeEventListener('mousemove', moveHandler);
document.removeEventListener('keydown', onKeyDown);
hoverId++;
}
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, currentRequestHoverId) {
if (currentIframe) {
currentIframe.src = 'about:blank';
currentIframe.remove();
currentIframe = null;
}
currentIframe = document.createElement('iframe');
Object.assign(currentIframe.style, {
position: 'fixed',
left: '-9999px',
width: '1px',
height: '1px',
visibility: 'hidden'
});
document.body.appendChild(currentIframe);
if (document.activeElement && typeof document.activeElement.blur === 'function') {
document.activeElement.blur();
}
currentIframe.onload = async () => {
if (currentRequestHoverId !== hoverId) {
return;
}
try {
const doc = currentIframe.contentDocument || currentIframe.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();
}
};
currentIframe.onerror = () => {
if (currentRequestHoverId === hoverId) {
hidePreview();
}
};
currentIframe.src = url;
document.addEventListener('mousemove', moveHandler);
document.addEventListener('keydown', onKeyDown);
moveHandler(event);
}
function handleMouseEnter(event, element, href) {
clearTimeout(hoverTimer);
hoverId++;
const currentRequestHoverId = hoverId;
hoverTimer = setTimeout(() => {
if (currentRequestHoverId !== hoverId) return;
const fullUrl = href.startsWith('http') ? href : location.origin + href;
showPreviewAtMouse(event, fullUrl, currentRequestHoverId);
}, 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');
document.head.appendChild(styleFix);
function updateGlobalStyles() {
const isDarkMode = document.documentElement.classList.contains('dark');
let css = `
.comment-wrapper,
.comment-wrapper .overflow-x-auto,
.comment-wrapper .overflow-hidden,
.thread-body-content .overflow-hidden {
overflow: visible !important;
max-height: none !important;
}
`;
if (isDarkMode) {
css += `
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;
}
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 = '';
}
}
}
const mainObserver = new MutationObserver((mutationsList) => {
if (mutationsList.some(m => m.target === document.documentElement && m.attributeName === 'class')) {
updateGlobalStyles();
}
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;
hidePreview();
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/')) {
hidePreview();
setTimeout(() => {
runCommentFix();
attachEvents();
updateGlobalStyles();
addDarkmodeStyleToShadowDOMInternal();
}, 500);
}
};
window.addEventListener('popstate', () => {
if (location.href.includes('/s/')) {
hidePreview();
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);
}
})();