// ==UserScript==
// @name Kone gg Gallery Viewer
// @description 코네용 갤러리 뷰어
// @namespace http://tampermonkey.net/
// @version 1.4
// @author Mowa
// @match https://kone.gg/s/*
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @license MIT
// ==/UserScript==
(async function() {
'use strict';
// Singleton
class Kgv {
static getCanvasImage (imgElement, maxSize = 200) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const originalWidth = imgElement.naturalWidth || imgElement.width;
const originalHeight = imgElement.naturalHeight || imgElement.height;
const ratio = Math.min(maxSize / originalWidth, maxSize / originalHeight);
const newWidth = Math.round(originalWidth * ratio);
const newHeight = Math.round(originalHeight * ratio);
canvas.width = newWidth;
canvas.height = newHeight;
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
ctx.drawImage(imgElement, 0, 0, newWidth, newHeight);
return canvas;
}
static resizeImageToBase64(imgElement, maxSize = 200, outputFormat = 'image/jpeg', quality = 0.8) {
const canvas = Kgv.getCanvasImage(imgElement, maxSize);
return canvas.toDataURL(outputFormat, quality);
}
static getLargeImageData(imgElement, maxSize = 200) {
const canvas = Kgv.getCanvasImage(imgElement, maxSize);
return canvas.getContext('2d').getImageData(0, 0, canvas.width, canvas.height).data;
}
static calculateImageKernalHash(imageData, width, height, kernalSize = 10) {
const hash = new Uint8Array(256).fill(0);
const halfKernal = Math.floor(kernalSize / 2);
const totalPixels = kernalSize * kernalSize;
for (let y = halfKernal; y < height - halfKernal; y++) {
for (let x = halfKernal; x < width - halfKernal; x++) {
let rSum = 0, gSum = 0, bSum = 0;
for (let ky = -halfKernal; ky <= halfKernal; ky++) {
for (let kx = -halfKernal; kx <= halfKernal; kx++) {
const idx = ((y + ky) * width + (x + kx)) * 4;
rSum += imageData[idx];
gSum += imageData[idx + 1];
bSum += imageData[idx + 2];
}
}
const rAvg = Math.floor(rSum / totalPixels);
const gAvg = Math.floor(gSum / totalPixels);
const bAvg = Math.floor(bSum / totalPixels);
hash[rAvg]++;
hash[gAvg]++;
hash[bAvg]++;
}
}
return hash;
}
static calculateImageHistogram(imageData) {
const histogram = { r: new Array(256).fill(0), g: new Array(256).fill(0), b: new Array(256).fill(0) };
for (let i = 0; i < imageData.length; i += 4) {
histogram.r[imageData[i]]++;
histogram.g[imageData[i + 1]]++;
histogram.b[imageData[i + 2]]++;
}
return histogram;
}
// Credit: https://gf.qytechs.cn/en/scripts/536425-kone-%EC%8D%B8%EB%84%A4%EC%9D%BC-%EB%8C%93%EA%B8%80-%EA%B0%9C%EC%84%A0
static async 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) { }
}
// Credit: https://gf.qytechs.cn/en/scripts/536425-kone-%EC%8D%B8%EB%84%A4%EC%9D%BC-%EB%8C%93%EA%B8%80-%EA%B0%9C%EC%84%A0
static extractImagesFromIframeDocument(doc) {
const proseContainer = doc.querySelector('div.prose-container');
if (!proseContainer || !proseContainer.shadowRoot) {
return [];
}
const contentInShadow = proseContainer.shadowRoot.querySelector('div.dark');
if (!contentInShadow) {
return [];
}
return [...contentInShadow.querySelectorAll('img')]
.filter(img => (
img.src && !/kone-logo|default|placeholder|data:image/.test(img.src)
));
}
static relativeUrlToAbsolute (relativeUrl) {
if (!relativeUrl) return '';
try {
const baseUrl = window.location.origin + window.location.pathname;
return new URL(relativeUrl, baseUrl).href;
} catch (e) {
console.error('Invalid relative URL:', relativeUrl, e);
return '';
}
}
static filterOnlyPathUrl (url) {
if (!url) return '';
try {
const parsedUrl = new URL(url);
return parsedUrl.pathname;
} catch (e) {
console.error('Invalid URL:', url, e);
return '';
}
}
static kgvCSS = /* css */ `
.kgv-list {
width: 100%;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
align-items: flex-start;
align-content: flex-start;
gap: 0.2em;
}
.kgv-gallery {
display: inline-block;
width: 10.5em;
}
.kgv-gallery-good {
color: var(--color-red-400);
}
.kgv-gallery-bad {
color: #444;
}
.kgv-gallery-preview {
display: flex;
justify-content: center;
align-items: center;
width: 10.5em;
height: 10.5em;
overflow: hidden;
background-color: #777;
border-radius: 5px;
}
.kgv-gallery-preview img {
object-fit: cover;
width: 100%;
height: 100%;
}
.kgv-gallery-bad .kgv-gallery-preview > * {
filter: grayscale(100%) blur(10px);
}
.kgv-gallery-preview video {
object-fit: contain;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.kgv-gallery-info {
width: auto;
padding: 5px 0 0 0;
overflow: hidden;
font-size: 0.8rem;
line-height: 1.1;
}
.kgv-gallery-info-1 {
display: block;
}
.kgv-gallery-info-2 {
display: flex;
flex-direction: row;
gap: 0.2em;
}
.kgv-gallery-info-3 {
display: flex;
flex-direction: row;
gap: 0.2em;
margin-top: 0.2em;
}
.kgv-gallery-info-2 svg, .kgv-gallery-info-3 svg {
display: inline-block !important;
}
.kgv-title {
display: inline;
font-weight: bold;
line-height: 1.2;
}
.kgv-comment {
display: inline;
color: #777;
}
.kgv-author {
height: 0.8rem;
overflow: hidden;
}
.kgv-view {
color: #777;
}
.kgv-vote {
color: #777;
}
.kgv-gallery-bad .kgv-vote {
color: #f00;
}
.kgv-block {
display: none;
}
.kgv-gallery-bad .kgv-block {
display: inline-block;
color: #f00;
}
`;
// Instance start
static key = 'mowkgv'
static instance = null;
static defaultConfig = {
viewerType: 0, // 0: default(List), 1: Gallery
maxCacheImgUrls: 100000,
};
static async getInstance () { return Kgv.instance || (Kgv.instance = await Object.create(Kgv.prototype)).init(); }
constructor () { throw new Error(); }
listeners = {};
config = {};
cacheImgUrls = new Map();
previewIframe = null;
queuePreviewImgUrls = [];
queueTimeoutUid = null;
galleryViewListElement = null;
async init () {
// Due to Object.create
this.listeners = {};
this.config = {};
this.cacheImgUrls = new Map();
if (this.previewIframe) {
this.previewIframe.remove();
}
this.previewIframe = null;
this.queuePreviewImgUrls = [];
this.queueTimeoutUid = null;
this.galleryViewListElement = null;
await this.loadAllConfig();
this.loadCacheImgUrls();
GM_addStyle(Kgv.kgvCSS);
return this;
}
addEventListener(type, listener, once = false) {
if (!this.listeners[type]) {
this.listeners[type] = [];
}
if (once) {
const wrappedListener = (...args) => {
listener.apply(this, args);
this.removeEventListener(type, wrappedListener);
};
this.listeners[type].push(wrappedListener);
} else {
this.listeners[type].push(listener);
}
}
removeEventListener(type, listener) {
if (!this.listeners[type]) return;
const index = this.listeners[type].indexOf(listener);
if (index > -1) {
this.listeners[type].splice(index, 1);
}
}
dispatchEvent(event) {
if (!this.listeners[event.type]) return true;
this.listeners[event.type].forEach(listener => {
listener.call(this, event);
});
return true;
}
async loadAllConfig () { for (const [key, value] of Object.entries(Kgv.defaultConfig)) this.config[key] = GM_getValue(`${Kgv.key}_${key}`, value); }
async saveConfig (key, value) { GM_setValue(`${Kgv.key}_${key}`, this.config[key] = value); }
ensureCacheImgUrls () {
if (!this.cacheImgUrls) this.loadCacheImgUrls();
if (this.cacheImgUrls.size > this.config.maxCacheImgUrls) {
console.warn(`Cache size exceeded limit (${this.config.maxCacheImgUrls}), trimming cache.`);
}
while (this.cacheImgUrls.size > this.config.maxCacheImgUrls) {
this.cacheImgUrls.delete(this.cacheImgUrls.keys().next().value);
}
}
loadCacheImgUrls() {
try {
this.cacheImgUrls = new Map(JSON.parse(localStorage.getItem(`${Kgv.key}_cacheImgUrls`) || '[]'));
} catch (e) {
console.error('Failed to parse cacheImgUrls:', e);
this.cacheImgUrls = new Map();
}
}
saveCacheImgUrls() {
if (!this.cacheImgUrls) return;
try {
localStorage.setItem(`${Kgv.key}_cacheImgUrls`, JSON.stringify([...this.cacheImgUrls.entries()]));
} catch (e) {
if (e instanceof DOMException && e.name === 'QuotaExceededError') {
if (this.config.maxCacheImgUrls > 100) {
let nextLimit = Math.floor(this.cacheImgUrls.size * 0.9);
if (nextLimit < 100) nextLimit = 100;
this.saveConfig('maxCacheImgUrls', nextLimit);
return this.saveCacheImgUrls();
}
}
console.error('Failed to save cacheImgUrls:', e);
}
}
// null: no image, undefined: not cached
getCacheImgUrl (url) {
if (!url) return undefined;
return this.cacheImgUrls.get(Kgv.filterOnlyPathUrl(url));
}
pickPreviewCandidate (imgElements) {
console.debug('Picking preview candidate from:', imgElements);
if (!imgElements || imgElements.length === 0) return null;
// TODO: remove mibang
return imgElements[0];
}
async crawlPreviewImgUrls (url) {
return new Promise((resolve, _) => {
const finalize = (resultUrl) => {
this.previewIframe.remove();
this.previewIframe = null;
return resolve(resultUrl);
}
url = Kgv.relativeUrlToAbsolute(url);
if (this.previewIframe) {
this.previewIframe.remove();
this.previewIframe = null;
}
this.previewIframe = document.createElement('iframe');
Object.assign(this.previewIframe.style, {
position: 'fixed',
left: '-9999px',
width: '1px',
height: '1px',
visibility: 'hidden',
});
document.body.appendChild(this.previewIframe);
this.previewIframe.onload = async () => {
console.debug('Preview iframe loaded:', url);
const retryLoop = async (maxRetries = 20, delay = 100) => {
try {
const doc = this.previewIframe.contentDocument || this.previewIframe.contentWindow.document;
const shadowRoot = doc?.querySelector('.prose-container')
if (shadowRoot) {
await Kgv.handleModalsInIframeKone(doc);
const previewElement = this.pickPreviewCandidate(Kgv.extractImagesFromIframeDocument(doc));
if (!previewElement || !previewElement.src) {
console.warn('No valid preview image found in iframe document:', url);
return finalize(null);
}
return finalize(previewElement.src);
} else {
return setTimeout(() => {
if (maxRetries > 0) {
console.debug('Retrying to load iframe content, remaining retries:', maxRetries);
return retryLoop(maxRetries - 1, delay);
} else {
console.warn('Max retries reached, no valid content found in iframe document:', url);
return finalize(null);
}
}, delay);
}
} catch (e) {
console.error('Error loading iframe document:', e);
return finalize(null);
}
};
await retryLoop();
};
this.previewIframe.onerror = (e) => {
console.error('Error loading iframe:', e);
return finalize(null);
};
this.previewIframe.src = url;
});
}
async runQueuePreviewImgUrls () {
if (this.queueTimeoutUid) {
console.debug('Queue is already running, skipping this run.');
return;
} else if (this.queuePreviewImgUrls.length === 0) {
console.debug('No URLs in queue to process.');
return;
}
this.queueTimeoutUid = -1;
const nextUrl = this.queuePreviewImgUrls.pop();
if (nextUrl) {
console.debug('Processing URL from queue:', nextUrl);
try {
const previewUrl = await this.crawlPreviewImgUrls(nextUrl);
this.cacheImgUrls.set(Kgv.filterOnlyPathUrl(nextUrl), previewUrl || null);
this.ensureCacheImgUrls();
this.saveCacheImgUrls();
this.dispatchEvent(new CustomEvent('previewImgUrlCrawled', {
detail: { url: nextUrl, previewUrl: previewUrl }
}));
} catch (e) {
console.error('Error processing URL in queue:', nextUrl, e);
}
}
this.queueTimeoutUid = setTimeout(() => {
this.queueTimeoutUid = null;
this.runQueuePreviewImgUrls();
}, 0);
}
requestQueuePreviewImgUrl (url) {
if (!url) {
console.warn('Invalid URL requested for preview image:', url);
return;
}
const cachedImgUrl = this.getCacheImgUrl(url);
if (cachedImgUrl !== undefined) {
console.debug('Using cached preview image URL:', url, cachedImgUrl);
this.dispatchEvent(new CustomEvent('previewImgUrlCrawled', {
detail: { url: url, previewUrl: cachedImgUrl }
}));
} else {
this.queuePreviewImgUrls.push(url);
this.runQueuePreviewImgUrls();
}
}
koneParseGalleryInfoList (list) {
const resultGalleryInfo = [];
for (const item of list) {
const galleryInfo = {
link: item.querySelector('a')?.href || '',
badgeHtml: item.querySelector('a > div:nth-child(1) > div > div.items-stretch > span.justify-center.border')?.outerHTML || '',
title: item.querySelector('a > div:nth-child(1) > div > div.items-stretch > .text-ellipsis')?.innerHTML || '',
commentCountStr: item.querySelector('a > div:nth-child(1) > div > div.items-stretch > span.text-xs')?.innerHTML || '',
author: item.querySelector('a > div:nth-child(1) > div:nth-child(2)')?.innerHTML || '',
timeStr: item.querySelector('a > div:nth-child(1) > div:nth-child(3)')?.innerHTML || '',
viewStr: item.querySelector('a > div:nth-child(1) > div:nth-child(4)')?.innerHTML || '',
rating: parseInt(item.querySelector('a > div:nth-child(1) > div:nth-child(5)')?.innerHTML.replace(/[^0-9\-]/g, '')) || 0,
isRatingHigh: item.querySelector('a > div:nth-child(1) > div:nth-child(5)')?.classList.contains('text-red-500') || false,
isRatingLow: false,
};
if (galleryInfo.rating < 0) {
galleryInfo.isRatingLow = true;
}
resultGalleryInfo.push(galleryInfo);
}
return resultGalleryInfo;
}
buildGalleryCard (galleryInfo) {
const card = document.createElement('a');
card.href = galleryInfo.link;
card.classList.add('kgv-gallery');
if (galleryInfo.isRatingHigh) {
card.classList.add('kgv-gallery-good');
}
if (galleryInfo.isRatingLow) {
card.classList.add('kgv-gallery-bad');
}
card.innerHTML = /*html*/ `
<div class="kgv-gallery-preview">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 -960 960 960" fill="currentColor"><path d="M320-160h320v-120q0-66-47-113t-113-47q-66 0-113 47t-47 113v120Zm160-360q66 0 113-47t47-113v-120H320v120q0 66 47 113t113 47ZM160-80v-80h80v-120q0-61 28.5-114.5T348-480q-51-32-79.5-85.5T240-680v-120h-80v-80h640v80h-80v120q0 61-28.5 114.5T612-480q51 32 79.5 85.5T720-280v120h80v80H160Z"/></svg>
</div>
<div class="kgv-gallery-info">
<div class="kgv-gallery-info-1">
<span class="kgv-title">${galleryInfo.title}</span>
<span class="kgv-comment">
${galleryInfo.commentCountStr}
</span>
</div>
<div class="kgv-gallery-info-2">
<span class="kgv-author">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 -960 960 960" fill="currentColor"><path d="M480-480q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 66-47 113t-113 47ZM160-160v-112q0-34 17.5-62.5T224-378q62-31 126-46.5T480-440q66 0 130 15.5T736-378q29 15 46.5 43.5T800-272v112H160Zm80-80h480v-32q0-11-5.5-20T700-306q-54-27-109-40.5T480-360q-56 0-111 13.5T260-306q-9 5-14.5 14t-5.5 20v32Zm240-320q33 0 56.5-23.5T560-640q0-33-23.5-56.5T480-720q-33 0-56.5 23.5T400-640q0 33 23.5 56.5T480-560Zm0-80Zm0 400Z"/></svg>
${galleryInfo.author}
</span>
</div>
<div class="kgv-gallery-info-3">
<span class="kgv-category">
${galleryInfo.badgeHtml || ''}
</span>
<span class="kgv-view">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 512 512"><circle cx="256" cy="256" r="64" fill="currentColor"/><path fill="currentColor" d="M490.84 238.6c-26.46-40.92-60.79-75.68-99.27-100.53C349 110.55 302 96 255.66 96c-42.52 0-84.33 12.15-124.27 36.11c-40.73 24.43-77.63 60.12-109.68 106.07a31.92 31.92 0 0 0-.64 35.54c26.41 41.33 60.4 76.14 98.28 100.65C162 402 207.9 416 255.66 416c46.71 0 93.81-14.43 136.2-41.72c38.46-24.77 72.72-59.66 99.08-100.92a32.2 32.2 0 0 0-.1-34.76ZM256 352a96 96 0 1 1 96-96a96.11 96.11 0 0 1-96 96Z"/></svg>
${galleryInfo.viewStr}
</span>
<span class="kgv-vote">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 512 512"><path fill="currentColor" d="M456 128a40 40 0 0 0-37.23 54.6l-84.17 84.17a39.86 39.86 0 0 0-29.2 0l-60.17-60.17a40 40 0 1 0-74.46 0L70.6 306.77a40 40 0 1 0 22.63 22.63L193.4 229.23a39.86 39.86 0 0 0 29.2 0l60.17 60.17a40 40 0 1 0 74.46 0l84.17-84.17A40 40 0 1 0 456 128Z"/></svg>
${galleryInfo.rating}
</span>
</div>
</div>
`;
const onCrawled = (e) => {
if (Kgv.filterOnlyPathUrl(e.detail.url) === Kgv.filterOnlyPathUrl(galleryInfo.link)) {
this.removeEventListener('previewImgUrlCrawled', onCrawled);
const previewElement = card.querySelector('.kgv-gallery-preview');
if (previewElement) {
previewElement.innerHTML = '';
if (e.detail.previewUrl) {
const imgElement = document.createElement('img');
imgElement.loading = 'lazy';
imgElement.src = e.detail.previewUrl;
previewElement.appendChild(imgElement);
}
}
}
}
this.addEventListener('previewImgUrlCrawled', onCrawled);
this.requestQueuePreviewImgUrl(galleryInfo.link);
return card;
}
renderGalleryList () {
const listContainer = document.querySelector('div.grow.flex.flex-col.overflow-hidden.relative > div.grow');
const list = document.querySelectorAll('div.grow.flex.flex-col.overflow-hidden.relative > div.grow > div.w-full');
if (!listContainer || !list) {
console.warn('List container or list not found.');
return;
}
const galleryInfoList = this.koneParseGalleryInfoList(list);
if (galleryInfoList.length === 0) {
console.warn('No gallery info found.');
return;
}
if (this.galleryViewListElement) {
this.galleryViewListElement.remove();
this.galleryViewListElement = null;
}
this.galleryViewListElement = document.createElement('div');
this.galleryViewListElement.classList.add('kgv-list');
galleryInfoList.map(this.buildGalleryCard.bind(this)).forEach(card => {
this.galleryViewListElement.appendChild(card);
});
listContainer.after(this.galleryViewListElement);
listContainer.style.display = 'none';
}
observeURLChange() {
let lastUrl = location.href;
const onURLChange = () => {
setTimeout(() => {
console.debug('URL changed, re-rendering gallery list:', lastUrl);
kgvInstance.renderGalleryList();
}, 500);
}
const urlChangeHandler = () => {
if (location.href !== lastUrl && location.href.includes('/s/')) {
lastUrl = location.href;
onURLChange();
}
};
const urlObserver = new MutationObserver(urlChangeHandler);
urlObserver.observe(document.body, { childList: true, subtree: true });
const originalPush = history.pushState;
history.pushState = function () {
originalPush.apply(this, arguments);
urlChangeHandler();
};
window.addEventListener('popstate', urlChangeHandler);
onURLChange(); // Initial call to render on script load
}
}
// Initialize the Kgv instance
const kgvInstance = await Kgv.getInstance();
if (document.readyState === 'complete' || document.readyState === 'interactive') {
kgvInstance.observeURLChange();
} else {
document.addEventListener('DOMContentLoaded', () => {
kgvInstance.observeURLChange();
});
}
})();