Kone gg Gallery Viewer

코네용 갤러리 뷰어

// ==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://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 {
                        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://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 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://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;

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

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址