Kone gg Gallery Viewer

코네용 갤러리 뷰어

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Kone gg Gallery Viewer
// @description  코네용 갤러리 뷰어
// @namespace    http://tampermonkey.net/
// @version      2.3
// @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 createImageFromArrayBuffer(arrayBuffer) {
        //     // ArrayBuffer를 Uint8Array로 변환
        //     const uint8Array = new Uint8Array(arrayBuffer);
            
        //     // Base64로 인코딩
        //     const base64String = btoa(String.fromCharCode(...uint8Array));
            
        //     // Data URL 생성
        //     const dataUrl = `data:image/jpeg;base64,${base64String}`;
            
        //     // img element 생성
        //     const img = document.createElement('img');
        //     img.src = dataUrl;
            
        //     return img;
        // }

        // static arrayBufferToString(arrayBuffer) {
        //     // ArrayBuffer를 Uint8Array로 변환
        //     const uint8Array = new Uint8Array(arrayBuffer);
            
        //     // 문자열로 변환
        //     let str = '';
        //     for (let i = 0; i < uint8Array.length; i++) {
        //         str += String.fromCharCode(uint8Array[i]);
        //     }
        //     return str;
        // }
    
        // static async getCanvasImage (imgSrc, maxSize = 200) {
        //     return new Promise((resolve, reject) => {
        //         GM_xmlhttpRequest({
        //             method: 'GET',
        //             url: imgSrc,
        //             responseType: 'arraybuffer',
        //             // headers: {
        //             //     'Accept': 'image/webp,image/apng,image/*,*/*;q=0.8',
        //             //     'Accept-Encoding': 'gzip, deflate, br',
        //             //     'Accept-Language': navigator.language || navigator.userLanguage,
        //             //     'Cookie': document.cookie,
        //             //     'Host': window.location.host,
        //             //     'Referer': window.location.href,
        //             //     'User-Agent': navigator.userAgent,
        //             // },
        //             onload: (response) => {
        //                 console.log(response.response);
        //                 console.log(Kgv.arrayBufferToString(response.response));
        //                 const imgElement = Kgv.createImageFromArrayBuffer(response.response)
        //                 imgElement.onload = () => {
        //                     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);
        //                     resolve(canvas);
        //                 };
        //                 imgElement.onerror = (e) => {
        //                     console.error('Error XHR loading image:', e);
        //                     reject(e);
        //                 };
        //                 document.body.appendChild(imgElement); // Append to body to avoid CORS issues
        //             },
        //             onerror: (e) => {
        //                 console.error('Error XHR fetching image:', e);
        //                 reject(e);
        //             }
        //         })
        //     });
        // }

        // static async resizeImageToBase64 (imgSrc, maxSize = 200, outputFormat = 'image/jpeg', quality = 0.8) {
        //     const canvas = await Kgv.getCanvasImage(imgSrc, maxSize);
        //     return canvas.toDataURL(outputFormat, quality);
        // }

        // static async getLargeImageData (imgSrc, maxSize = 200) {
        //     const canvas = await Kgv.getCanvasImage(imgSrc, maxSize);
        //     return [canvas.getContext('2d').getImageData(0, 0, canvas.width, canvas.height).data, canvas.width, canvas.height];
        // }

        // static async calculateImageDiffHash (imgSrc, hashSize = 16) {
        //     const getBrightness = (imageData, width, x, y) => {
        //         const idx = (y * width + x) * 4;
        //         return (imageData[idx] + imageData[idx + 1] + imageData[idx + 2]) / 3; // Average RGB
        //     }
            
        //     const [imageData, width, height] = await Kgv.getLargeImageData(imgSrc, hashSize);
        //     let hash = new Array(hashSize*hashSize).fill(0).join('');
        //     for (let y = 0; y < height; y += 1) {
        //         for (let x = 0; x < width-1; x += 1) {
        //             hash[y * hashSize + x] = Math.floor(
        //                 (
        //                     (getBrightness(imageData, width, x, y) - getBrightness(imageData, width, x + 1, y))
        //                     + 256
        //                 ) / 512 * 10
        //             );
        //         }
        //     }
        //     return hash;
        // }

        // static diffHashHammingDistanceNormal(hash1, hash2) {
        //     const min = Math.min(hash1.length, hash2.length);
        //     let distance = 0;
        //     for (let i = 0; i < min; i++) {
        //         if (hash1[i] !== hash2[i]) {
        //             distance++;
        //         }
        //     }
        //     return distance / min;
        // }

        // static async calculateImageHistogram (imgSrc, hashSize = 16) {
        //     const [imageData, width, height] = await Kgv.getLargeImageData(imgSrc, hashSize);

        //     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;
        // }

        // static histogramToString (histogram) {
        //     const combined = [...histogram.r, ...histogram.g, ...histogram.b];
        //     const uint16Array = new Uint16Array(combined);
        //     const bytes = new Uint8Array(uint16Array.buffer);
        //     let binary = '';
        //     for (let i = 0; i < bytes.length; i++) {
        //         binary += String.fromCharCode(bytes[i]);
        //     }
        //     return btoa(binary);
        // }

        // static stringToHistogram (str) {
        //     // Base64 디코딩
        //     const binary = atob(str);
        //     const bytes = new Uint8Array(binary.length);
        //     for (let i = 0; i < binary.length; i++) {
        //         bytes[i] = binary.charCodeAt(i);
        //     }
        //     const uint16Array = new Uint16Array(bytes.buffer);
        //     const combined = Array.from(uint16Array);
            
        //     return {
        //         r: combined.slice(0, 256),
        //         g: combined.slice(256, 512),
        //         b: combined.slice(512, 768)
        //     };
        // }

        // static compareHistogram (hist1, hist2) {
        //     let correlation = 0;
        //     const channels = ['r', 'g', 'b'];
            
        //     for (const channel of channels) {
        //         let sum1 = 0, sum2 = 0, sum1Sq = 0, sum2Sq = 0, pSum = 0;
                
        //         for (let i = 0; i < 256; i++) {
        //             sum1 += hist1[channel][i];
        //             sum2 += hist2[channel][i];
        //             sum1Sq += hist1[channel][i] * hist1[channel][i];
        //             sum2Sq += hist2[channel][i] * hist2[channel][i];
        //             pSum += hist1[channel][i] * hist2[channel][i];
        //         }
                
        //         const num = pSum - (sum1 * sum2 / 256);
        //         const den = Math.sqrt((sum1Sq - sum1 * sum1 / 256) * (sum2Sq - sum2 * sum2 / 256));
                
        //         if (den === 0) continue;
        //         correlation += num / den;
        //     }
            
        //     return correlation / 3;
        // }

        // Credit: https://greasyfork.org/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 {
                        Kgv.hideElementInIframe(doc, '.age-verification-popup');
                        Kgv.hideElementInIframe(doc, '.content-overlay.block');
                    }
                } else {
                    Kgv.hideElementInIframe(doc, '.age-verification-popup');
                    Kgv.hideElementInIframe(doc, '.content-overlay.block');
                }
            } catch (e) { }
        }

        // Credit: https://greasyfork.org/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 hideElementInIframe(doc, selector) {
            try {
                const elements = doc.querySelectorAll(selector);
                elements.forEach(el => {
                    if (el.offsetParent !== null) {
                        el.style.setProperty('display', 'none', 'important');
                    }
                });
            } catch (e) { }
        }

        // Credit: https://greasyfork.org/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;

            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;
            align-items: center;
            gap: 0.2em;
            margin-top: 0.2em;

            color: #777;
        }

        .kgv-gallery-info-2 svg, .kgv-gallery-info-3 svg {
            display: inline-block !important;
        }

        .kgv-gallery-info-3 > * {
            margin: 0 !important;
            padding: 0 !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-time {
            color: #777;
        }

        .kgv-gallery-bad .kgv-vote {
            color: #f00;
        }

        .kgv-block {
            display: none;
        }

        .kgv-gallery-bad .kgv-block {
            display: inline-block;

            color: #f00;
        }

        .kgv-menu {
            display: flex;
            flex-direction: row;
            gap: 0.5em;

            flex: 0 0 auto;
        }

        .kgv-menu-container {
            box-sizing: border-box;
            boder: 0 solid;
            margin: 0;
            padding: 0;
            display: flex;
            flex-direction: row;

            border: 1px solid #777;
            border-radius: 5px;
        }

        .kgv-menu-container > .kgv-menu-container-btn {
            flex: 1;

            padding: 0.4em 0.2em;

            border-left: 1px solid #777;

            cursor: pointer;
            text-align: center;
            font-size: 0.7em;
        }

        .kgv-menu-container > .kgv-menu-container-btn:first-child {
            border-left: none;
        }

        .kgv-menu-container > .kgv-menu-container-btn.active {
            background-color: #007bff;

            color: #fff;
        }

        .kgv-menu-btn {
            padding: 0.4em 0.2em;

            border: 1px solid #777;
            border-radius: 5px;

            cursor: pointer;
            font-size: 0.7em;
        }

        .kgv-mibang-handle {
            border: 5px solid #007bff !important;
            cursor: pointer;
        }
        `;

        // Instance start

        static key = 'mowkgv'
        static keyCacheImgUrls = `${Kgv.key}_cacheImgUrls`;
        static keyMibang = `${Kgv.key}_mibang`;
        static instance = null;
        static defaultConfig = {
            viewerType: 1, // 0: default(List), 1: Gallery
            maxCacheImgUrls: 100000,
        };
        static qMain = 'main > div.mx-auto > div.mx-auto > div.flex > div.flex';
        static qSubTitle =  'main > div:nth-child(1) > div:nth-child(1) > div:nth-child(1) > div:nth-child(1) > div:nth-child(1)';
        static qSubMenu = 'main .py-2 .p-3:nth-child(1)';
        static qSubMenuBtns = 'main .py-2 .p-3:nth-child(1) > .items-center';
        static qSubMenuSearch = 'main .py-2 .p-3:nth-child(2)';
        static qSubCategory = 'main div[dir=ltr]';
        static qSubMain = 'div.h-full.flex-col';
        static qSubMainListContainer = 'div.h-full.flex-col > div.grow.flex-col > div.grow';
        static qSubMainList = 'div.h-full.flex-col > div.grow.flex-col > div.grow > div.w-full';
        static async getInstance () { return Kgv.instance || (Kgv.instance = await Object.create(Kgv.prototype)).init(); }
        constructor () { throw new Error(); }

        listeners = {};
        config = {};
        cacheImgUrls = new Map();
        mibang = [];
        previewIframe = null;
        queuePreviewImgUrls = [];
        queueTimeoutUid = null;
        galleryViewListElement = null;
        originListMutationObserver = null;
        popupMibang = null;

        async init () {
            // Due to Object.create
            this.listeners = {};
            this.config = {};
            this.cacheImgUrls = new Map();
            this.mibang = [];
            if (this.previewIframe) {
                this.previewIframe.remove();
            }
            this.previewIframe = null;
            this.queuePreviewImgUrls = [];
            this.queueTimeoutUid = null;
            this.galleryViewListElement = null;
            this.originListMutationObserver = null;
            this.popupMibang = null;

            this.loadAllConfig();
            // this.loadAllMibang();
            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;
        }

        loadAllConfig () { for (const [key, value] of Object.entries(Kgv.defaultConfig)) this.config[key] = GM_getValue(`${Kgv.key}_${key}`, value); }
        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.keyCacheImgUrls) || '[]'));
            } catch (e) {
                console.error('Failed to parse cacheImgUrls:', e);
                this.cacheImgUrls = new Map();
            }
        }

        saveCacheImgUrls() {
            if (!this.cacheImgUrls) return;
            try {
                localStorage.setItem(Kgv.keyCacheImgUrls, 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));
        }

        // loadAllMibang () {
        //     try {
        //         this.mibang = JSON.parse(localStorage.getItem(Kgv.keyMibang) || '[]');
        //     } catch (e) {
        //         console.error('Failed to parse mibang:', e);
        //         this.mibang = [];
        //     }
        // }

        // saveAllMibang () {
        //     if (!this.mibang) return;
        //     try {
        //         localStorage.setItem(Kgv.keyMibang, JSON.stringify(this.mibang));
        //     } catch (e) {
        //         console.error('Failed to save mibang:', e);
        //     }
        // }

        // saveMibang(imgSrc) {
        //     const previewBase64 = Kgv.resizeImageToBase64(imgSrc);
        //     const diffHash = Kgv.calculateImageDiffHash(imgSrc);
        //     const histogram = Kgv.histogramToString(Kgv.calculateImageHistogram(imgSrc));
            
        //     this.mibang.push({
        //         previewBase64,
        //         diffHash,
        //         histogram,
        //     });
        //     this.saveAllMibang();
        // }

        // checkIsMibang (imgSrc) {
        //     if (!this.mibang || this.mibang.length === 0) {
        //         return false;
        //     }
        //     const diffHash = Kgv.calculateImageDiffHash(imgSrc);
        //     const histogram = Kgv.calculateImageHistogram(imgSrc);
        //     for (const item of this.mibang) {
        //         if (Kgv.diffHashHammingDistanceNormal(item.diffHash, diffHash) < 0.1 &&
        //             Kgv.compareHistogram(Kgv.stringToHistogram(item.histogram), histogram) > 0.9) {
        //             return true;
        //         }
        //     }
        // }

        buildMenu () {
            const menuLoop = (max_retries = 20, delay = 100) => {
                const subMenuBtns = document.querySelector(Kgv.qSubMenuBtns);
                if (!subMenuBtns) {
                    if (max_retries > 0) {
                        return setTimeout(() => menuLoop(max_retries - 1, delay), delay);
                    } else {
                        console.warn('Max retries reached, subMenuBtns not found.');
                        return;
                    }
                }
                Object.assign(subMenuBtns.style, {
                    display: 'flex',
                    flexDirection: 'row',
                    flexWrap: 'wrap',
                })

                let customMenu = subMenuBtns.querySelector('.kgv-menu');
                if (!customMenu) {
                    customMenu = document.createElement('div');
                    customMenu.classList.add('kgv-menu');
                    subMenuBtns.appendChild(customMenu);
                } else {
                    customMenu.innerHTML = '';
                }
                
                const viewStyleContainer = document.createElement('div');
                viewStyleContainer.classList.add('kgv-menu-container');

                const viewStyleElem0 = document.createElement('button');
                viewStyleElem0.classList.add('kgv-menu-container-btn');
                if (this.config.viewerType === 0) {
                    viewStyleElem0.classList.add('active');
                }
                viewStyleElem0.textContent = '📜 리스트'
                viewStyleElem0.addEventListener('click', async () => {
                    if (this.config.viewerType === 0) {
                        console.debug('Already in list view, no action taken.');
                        return;
                    }
                    this.saveConfig('viewerType', 0);
                    // Refresh page
                    window.location.reload();
                    
                });
                viewStyleContainer.appendChild(viewStyleElem0);

                const viewStyleElem1 = document.createElement('button');
                viewStyleElem1.classList.add('kgv-menu-container-btn');
                if (this.config.viewerType === 1) {
                    viewStyleElem1.classList.add('active');
                }
                viewStyleElem1.textContent = '🖼️ 갤러리';
                viewStyleElem1.addEventListener('click', async () => {
                    if (this.config.viewerType === 1) {
                        console.debug('Already in gallery view, no action taken.');
                        return;
                    }
                    this.saveConfig('viewerType', 1);
                    // Refresh page
                    window.location.reload();
                });
                viewStyleContainer.appendChild(viewStyleElem1);
                customMenu.appendChild(viewStyleContainer);

                const cacheClearBtn = document.createElement('button');
                cacheClearBtn.classList.add('kgv-menu-btn');
                cacheClearBtn.textContent = '🗑️ 캐시제거';
                cacheClearBtn.addEventListener('click', () => {
                    if (confirm('캐시된 이미지 URL을 모두 삭제하시겠습니까?')) {
                        this.cacheImgUrls.clear();
                        this.saveCacheImgUrls();
                        // Refresh page
                        window.location.reload();
                    }
                });
                customMenu.appendChild(cacheClearBtn);

                subMenuBtns.appendChild(customMenu);

                // const mibandAddBtn = document.createElement('button');
                // mibandAddBtn.classList.add('kgv-menu-btn');
                // mibandAddBtn.textContent = '➕ 미방추가';
                // mibandAddBtn.addEventListener('click', () => {
                //     if (mibandAddBtn.classList.contains('active')) {
                //         return;
                //     }
                //     mibandAddBtn.classList.add('active');
                //     this.addMibangState();
                // });
                // customMenu.appendChild(mibandAddBtn);
                
                // const mibangBtn = document.createElement('button');
                // mibangBtn.classList.add('kgv-menu-btn');
                // mibangBtn.textContent = '🪄 미방';
                // mibangBtn.addEventListener('click', () => {
                //     if (this.popupMibang) {
                //         this.popupMibang.remove();
                //         this.popupMibang = null;
                //     } else {
                //         this.buildPopupMibang();
                //     }
                // });
                // customMenu.appendChild(mibangBtn);
            };
            menuLoop();
        }

        // addMibangState () {
        //     alert('이미지 클릭 시 미방에 추가되며, 미리보기에서 제외됩니다.\n취소하려면 새로고침하세요.');
        //     const allImgElements = document.querySelectorAll('img');
        //     if (!allImgElements || allImgElements.length === 0) {
        //         console.warn('No image elements found to add Mibang state.');
        //         return;
        //     }

        //     allImgElements.forEach((imgElement) => {
        //         if (imgElement.classList.contains('kgv-mibang-handle')) return;
        //         imgElement.classList.add('kgv-mibang-handle');
        //         imgElement.onclick = (e) => {
        //             e.stopPropagation();
        //             e.preventDefault();
                    
        //             this.saveMibang(imgElement.src);
        //             // refresh page
        //             window.location.reload();
        //         };
        //     });
        // }

        // buildPopupMibang () {
        //     if (this.popupMibang) {
        //         this.popupMibang.remove();
        //         this.popupMibang = null;
        //     }
        //     this.popupMibang = document.createElement('div');
        //     this.popupMibang.classList.add('kgv-popup-mibang');
        //     this.popupMibang.style.position = 'fixed';
        //     this.popupMibang.style.top = '50%';
        //     this.popupMibang.style.left = '50%';
        //     this.popupMibang.style.transform = 'translate(-50%, -50%)';
        //     this.popupMibang.style.backgroundColor = '#fff';
        //     this.popupMibang.style.padding = '1em';
        //     this.popupMibang.style.borderRadius = '5px';
        //     this.popupMibang.style.zIndex = '1000';

        //     const title = document.createElement('h2');
        //     title.textContent = '미방 설정';
        //     this.popupMibang.appendChild(title);

        //     const mibangList = document.createElement('ul');
        //     mibangList.classList.add('kgv-mibang-list');
        // }
        
        pickPreviewCandidate (imgElements) {
            console.debug('Picking preview candidate from:', imgElements);
            if (!imgElements || imgElements.length === 0) return null;
            // for (const imgElement of imgElements) {
            //     if (this.checkIsMibang(imgElement.src)) {
            //         console.debug('Found Mibang image, skipping:', imgElement);
            //         continue;
            //     }
            //     return imgElement;
            // }
            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) {
                return;
            } else if (this.queuePreviewImgUrls.length === 0) {
                console.debug('No URLs in queue to process.');
                return;
            }

            this.queueTimeoutUid = -1;
            const nextUrl = this.queuePreviewImgUrls.shift();
            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) {
                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 .contents > div:nth-child(1) > div:nth-child(1) > div:nth-child(1)')?.outerHTML || '',
                    title: item.querySelector('a .contents > div:nth-child(1) div.flex.items-center > span.text-ellipsis')?.innerHTML || '',
                    commentCountStr: item.querySelector('a .contents > div:nth-child(1) div.flex > span.text-xs')?.innerHTML || '',
                    author: item.querySelector('a .contents > div:nth-child(1) > div:nth-child(3) .text-ellipsis')?.innerHTML || '',
                    timeStr: item.querySelector('a .contents > div:nth-child(1) > div:nth-child(4)')?.innerHTML || '',
                    viewStr: item.querySelector('a .contents > div:nth-child(1) > div:nth-child(5)')?.innerHTML || '',
                    rating: parseInt(item.querySelector('a .contents > div:nth-child(1) > div:nth-child(6)')?.innerHTML.replace(/[^0-9\-]/g, '')) || 0,
                    isRatingHigh: item.querySelector('a .contents > div:nth-child(1) > div:nth-child(6)')?.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>
        <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>
        <span class="kgv-view">
            ${galleryInfo.viewStr}
        </span>
        <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>
        <span class="kgv-vote">
            ${galleryInfo.rating}
        </span>
        <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 512 512"><path fill="currentColor" d="M256 48C141.31 48 48 141.31 48 256s93.31 208 208 208s208-93.31 208-208S370.69 48 256 48zm0 368c-88.22 0-160-71.78-160-160s71.78-160 160-160s160 71.78 160 160s-71.78 160-160 160z"/><path fill="currentColor" d="M272 144h-32v144l96.97 58.18l16.03-26.28L272 240V144z"/></svg>
        <span class="kgv-time">
            ${galleryInfo.timeStr}
        </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 loadingLoop = (max_retries = 20, delay = 100) => {
                const listContainer = document.querySelector(Kgv.qSubMainListContainer);
                const list = document.querySelectorAll(Kgv.qSubMainList);
                if (listContainer && list && list.length > 0) {
                    if (this.originListMutationObserver) {
                        this.originListMutationObserver.disconnect();
                        this.originListMutationObserver = null;
                    }
                    this.originListMutationObserver = new MutationObserver(() => {
                        console.debug('Gallery list changed, re-rendering gallery list.');
                        this.buildMenu();
                        this.renderGalleryList();
                    });
                    this.originListMutationObserver.observe(listContainer, {
                        childList: true,
                        subtree: true,
                    });

                    // Reset queue
                    this.queuePreviewImgUrls = [];
                    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';
                } else {
                    if (max_retries > 0) {
                        return setTimeout(() => loadingLoop(max_retries - 1, delay), delay);
                    } else {
                        console.warn('Max retries reached, gallery list not found.');
                    }
                }
            }

            if (this.config.viewerType === 1) {
                loadingLoop();
            }
        }

        observeURLChange() {
            let lastUrl = location.href;

            const onURLChange = () => {
                setTimeout(() => {
                    console.debug('URL changed, re-rendering gallery list:', lastUrl);
                    kgvInstance.buildMenu()
                    kgvInstance.renderGalleryList();
                }, 100);
            }

            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();
        });
    }
})();