Kone gg Gallery Viewer

코네용 갤러리 뷰어

当前为 2025-05-25 提交的版本,查看 最新版本

// ==UserScript==
// @name         Kone gg Gallery Viewer
// @description  코네용 갤러리 뷰어
// @namespace    http://tampermonkey.net/
// @version      1.4
// @author       Mowa
// @match        https://kone.gg/s/*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @license      MIT
// ==/UserScript==
(async function() {
    'use strict';
    // Singleton
    class Kgv {
        static getCanvasImage (imgElement, maxSize = 200) {
            const canvas = document.createElement('canvas');
            const ctx = canvas.getContext('2d');
            const originalWidth = imgElement.naturalWidth || imgElement.width;
            const originalHeight = imgElement.naturalHeight || imgElement.height;
            const ratio = Math.min(maxSize / originalWidth, maxSize / originalHeight);
            const newWidth = Math.round(originalWidth * ratio);
            const newHeight = Math.round(originalHeight * ratio);
            canvas.width = newWidth;
            canvas.height = newHeight;
            ctx.imageSmoothingEnabled = true;
            ctx.imageSmoothingQuality = 'high';
            ctx.drawImage(imgElement, 0, 0, newWidth, newHeight);
            return canvas;
        }

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

        static getLargeImageData(imgElement, maxSize = 200) {
            const canvas = Kgv.getCanvasImage(imgElement, maxSize);
            return canvas.getContext('2d').getImageData(0, 0, canvas.width, canvas.height).data;
        }

        static calculateImageKernalHash(imageData, width, height, kernalSize = 10) {
            const hash = new Uint8Array(256).fill(0);
            const halfKernal = Math.floor(kernalSize / 2);
            const totalPixels = kernalSize * kernalSize;

            for (let y = halfKernal; y < height - halfKernal; y++) {
                for (let x = halfKernal; x < width - halfKernal; x++) {
                    let rSum = 0, gSum = 0, bSum = 0;

                    for (let ky = -halfKernal; ky <= halfKernal; ky++) {
                        for (let kx = -halfKernal; kx <= halfKernal; kx++) {
                            const idx = ((y + ky) * width + (x + kx)) * 4;
                            rSum += imageData[idx];
                            gSum += imageData[idx + 1];
                            bSum += imageData[idx + 2];
                        }
                    }

                    const rAvg = Math.floor(rSum / totalPixels);
                    const gAvg = Math.floor(gSum / totalPixels);
                    const bAvg = Math.floor(bSum / totalPixels);

                    hash[rAvg]++;
                    hash[gAvg]++;
                    hash[bAvg]++;
                }
            }

            return hash;
        }

        static calculateImageHistogram(imageData) {
            const histogram = { r: new Array(256).fill(0), g: new Array(256).fill(0), b: new Array(256).fill(0) };
            
            for (let i = 0; i < imageData.length; i += 4) {
                histogram.r[imageData[i]]++;
                histogram.g[imageData[i + 1]]++;
                histogram.b[imageData[i + 2]]++;
            }
            
            return histogram;
        }

        // Credit: https://gf.qytechs.cn/en/scripts/536425-kone-%EC%8D%B8%EB%84%A4%EC%9D%BC-%EB%8C%93%EA%B8%80-%EA%B0%9C%EC%84%A0
        static async handleModalsInIframeKone(doc) {
            try {
                const nsfwOverlayContainer = doc.querySelector('div.relative.min-h-60 > div.absolute.w-full.h-full.backdrop-blur-2xl');
                if (nsfwOverlayContainer && nsfwOverlayContainer.offsetParent !== null) {
                    const viewContentButton = nsfwOverlayContainer.querySelector('div.flex.gap-4 button:nth-child(2)');
                    if (viewContentButton && viewContentButton.textContent?.includes('콘텐츠 보기')) {
                        viewContentButton.click();
                        await new Promise(resolve => setTimeout(resolve, 500));
                    } else {
                        hideElementInIframe(doc, '.age-verification-popup');
                        hideElementInIframe(doc, '.content-overlay.block');
                    }
                } else {
                    hideElementInIframe(doc, '.age-verification-popup');
                    hideElementInIframe(doc, '.content-overlay.block');
                }
            } catch (e) { }
        }

        // Credit: https://gf.qytechs.cn/en/scripts/536425-kone-%EC%8D%B8%EB%84%A4%EC%9D%BC-%EB%8C%93%EA%B8%80-%EA%B0%9C%EC%84%A0
        static extractImagesFromIframeDocument(doc) {
            const proseContainer = doc.querySelector('div.prose-container');
            if (!proseContainer || !proseContainer.shadowRoot) {
                return [];
            }
            const contentInShadow = proseContainer.shadowRoot.querySelector('div.dark');
            if (!contentInShadow) {
                return [];
            }
            return [...contentInShadow.querySelectorAll('img')]
                .filter(img => (
                    img.src && !/kone-logo|default|placeholder|data:image/.test(img.src)
                ));
        }

        static relativeUrlToAbsolute (relativeUrl) {
            if (!relativeUrl) return '';
            try {
                const baseUrl = window.location.origin + window.location.pathname;
                return new URL(relativeUrl, baseUrl).href;
            } catch (e) {
                console.error('Invalid relative URL:', relativeUrl, e);
                return '';
            }
        }

        static filterOnlyPathUrl (url) {
            if (!url) return '';
            try {
                const parsedUrl = new URL(url);
                return parsedUrl.pathname;
            } catch (e) {
                console.error('Invalid URL:', url, e);
                return '';
            }
        }

        static kgvCSS = /* css */ `
    
        .kgv-list {
            width: 100%;
            display: flex;
            flex-direction: row;
            flex-wrap: wrap;
            justify-content: space-between;
            align-items: flex-start;
            align-content: flex-start;
            gap: 0.2em;
        }
        
        .kgv-gallery {
            display: inline-block;
            width: 10.5em;
        }

        .kgv-gallery-good {
            color: var(--color-red-400);
        }

        .kgv-gallery-bad {
            color: #444;
        }
        
        .kgv-gallery-preview {
            display: flex;
            justify-content: center;
            align-items: center;

            width: 10.5em;
            height: 10.5em;
            overflow: hidden;

            background-color: #777;
            border-radius: 5px;
        }

        .kgv-gallery-preview img {
            object-fit: cover;
            width: 100%;
            height: 100%;
        }

        .kgv-gallery-bad .kgv-gallery-preview > * {
            filter: grayscale(100%) blur(10px);
        }

        .kgv-gallery-preview video {
            object-fit: contain;
            width: 100%;
            height: 100%;

            display: flex;
            flex-direction: column;
        }
        
        .kgv-gallery-info {
            width: auto;
            padding: 5px 0 0 0;
            overflow: hidden;

            font-size: 0.8rem;
            line-height: 1.1;
        }

        .kgv-gallery-info-1 {
            display: block;
        }

        .kgv-gallery-info-2 {
            display: flex;
            flex-direction: row;
            gap: 0.2em;
        }

        .kgv-gallery-info-3 {
            display: flex;
            flex-direction: row;
            gap: 0.2em;
            margin-top: 0.2em;
        }

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

            font-weight: bold;
            line-height: 1.2;
        }

        .kgv-comment {
            display: inline;

            color: #777;
        }

        .kgv-author {
            height: 0.8rem;
            overflow: hidden;
        }

        .kgv-view {
            color: #777;
        }

        .kgv-vote {
            color: #777;
        }

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

        .kgv-block {
            display: none;
        }

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

            color: #f00;
        }
        
        `;

        // Instance start

        static key = 'mowkgv'
        static instance = null;
        static defaultConfig = {
            viewerType: 0, // 0: default(List), 1: Gallery
            maxCacheImgUrls: 100000,
        };
        static async getInstance () { return Kgv.instance || (Kgv.instance = await Object.create(Kgv.prototype)).init(); }
        constructor () { throw new Error(); }

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

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

            await this.loadAllConfig();
            this.loadCacheImgUrls();
            GM_addStyle(Kgv.kgvCSS);
            return this;
        }

        addEventListener(type, listener, once = false) {
            if (!this.listeners[type]) {
                this.listeners[type] = [];
            }
            if (once) {
                const wrappedListener = (...args) => {
                    listener.apply(this, args);
                    this.removeEventListener(type, wrappedListener);
                };
                this.listeners[type].push(wrappedListener);
            } else {
                this.listeners[type].push(listener);
            }
        }
        
        removeEventListener(type, listener) {
            if (!this.listeners[type]) return;
            const index = this.listeners[type].indexOf(listener);
            if (index > -1) {
                this.listeners[type].splice(index, 1);
            }
        }
        
        dispatchEvent(event) {
            if (!this.listeners[event.type]) return true;
            this.listeners[event.type].forEach(listener => {
                listener.call(this, event);
            });
            return true;
        }

        async loadAllConfig () { for (const [key, value] of Object.entries(Kgv.defaultConfig)) this.config[key] = GM_getValue(`${Kgv.key}_${key}`, value); }
        async saveConfig (key, value) { GM_setValue(`${Kgv.key}_${key}`, this.config[key] = value); }

        ensureCacheImgUrls () {
            if (!this.cacheImgUrls) this.loadCacheImgUrls();
            if (this.cacheImgUrls.size > this.config.maxCacheImgUrls) {
                console.warn(`Cache size exceeded limit (${this.config.maxCacheImgUrls}), trimming cache.`);
            }
            while (this.cacheImgUrls.size > this.config.maxCacheImgUrls) {
                this.cacheImgUrls.delete(this.cacheImgUrls.keys().next().value);
            }
        }

        loadCacheImgUrls() {
            try {
                this.cacheImgUrls = new Map(JSON.parse(localStorage.getItem(`${Kgv.key}_cacheImgUrls`) || '[]'));
            } catch (e) {
                console.error('Failed to parse cacheImgUrls:', e);
                this.cacheImgUrls = new Map();
            }
        }

        saveCacheImgUrls() {
            if (!this.cacheImgUrls) return;
            try {
                localStorage.setItem(`${Kgv.key}_cacheImgUrls`, JSON.stringify([...this.cacheImgUrls.entries()]));
            } catch (e) {
                if (e instanceof DOMException && e.name === 'QuotaExceededError') {
                    if (this.config.maxCacheImgUrls > 100) {
                        let nextLimit = Math.floor(this.cacheImgUrls.size * 0.9);
                        if (nextLimit < 100) nextLimit = 100;
                        this.saveConfig('maxCacheImgUrls', nextLimit);
                        return this.saveCacheImgUrls();
                    }
                }
                console.error('Failed to save cacheImgUrls:', e);
            }
        }

        // null: no image, undefined: not cached
        getCacheImgUrl (url) {
            if (!url) return undefined;
            return this.cacheImgUrls.get(Kgv.filterOnlyPathUrl(url));
        }
        
        pickPreviewCandidate (imgElements) {
            console.debug('Picking preview candidate from:', imgElements);
            if (!imgElements || imgElements.length === 0) return null;
            // TODO: remove mibang
            return imgElements[0];
        }

        async crawlPreviewImgUrls (url) {
            return new Promise((resolve, _) => {
                const finalize = (resultUrl) => {
                    this.previewIframe.remove();
                    this.previewIframe = null;
                    return resolve(resultUrl);
                }

                url = Kgv.relativeUrlToAbsolute(url);
                if (this.previewIframe) {
                    this.previewIframe.remove();
                    this.previewIframe = null;
                }
                this.previewIframe = document.createElement('iframe');
                Object.assign(this.previewIframe.style, {
                    position: 'fixed',
                    left: '-9999px',
                    width: '1px',
                    height: '1px',
                    visibility: 'hidden',
                });
                document.body.appendChild(this.previewIframe);

                this.previewIframe.onload = async () => {
                    console.debug('Preview iframe loaded:', url);

                    const retryLoop = async (maxRetries = 20, delay = 100) => {
                        try {
                            const doc = this.previewIframe.contentDocument || this.previewIframe.contentWindow.document;
                            const shadowRoot = doc?.querySelector('.prose-container')
                            if (shadowRoot) {
                                await Kgv.handleModalsInIframeKone(doc);
                                const previewElement = this.pickPreviewCandidate(Kgv.extractImagesFromIframeDocument(doc));
                                if (!previewElement || !previewElement.src) {
                                    console.warn('No valid preview image found in iframe document:', url);
                                    return finalize(null);
                                }
                                return finalize(previewElement.src);
                            } else {
                                return setTimeout(() => {
                                    if (maxRetries > 0) {
                                        console.debug('Retrying to load iframe content, remaining retries:', maxRetries);
                                        return retryLoop(maxRetries - 1, delay);
                                    } else {
                                        console.warn('Max retries reached, no valid content found in iframe document:', url);
                                        return finalize(null);
                                    }
                                }, delay);
                            }
                        } catch (e) {
                            console.error('Error loading iframe document:', e);
                            return finalize(null);
                        }
                    };
                    await retryLoop();
                };
                this.previewIframe.onerror = (e) => {
                    console.error('Error loading iframe:', e);
                    return finalize(null);
                };

                this.previewIframe.src = url;
            });
        }

        async runQueuePreviewImgUrls () {
            if (this.queueTimeoutUid) {
                console.debug('Queue is already running, skipping this run.');
                return;
            } else if (this.queuePreviewImgUrls.length === 0) {
                console.debug('No URLs in queue to process.');
                return;
            }

            this.queueTimeoutUid = -1;
            const nextUrl = this.queuePreviewImgUrls.pop();
            if (nextUrl) {
                console.debug('Processing URL from queue:', nextUrl);
                try {
                    const previewUrl = await this.crawlPreviewImgUrls(nextUrl);
                    this.cacheImgUrls.set(Kgv.filterOnlyPathUrl(nextUrl), previewUrl || null);
                    this.ensureCacheImgUrls();
                    this.saveCacheImgUrls();
                    this.dispatchEvent(new CustomEvent('previewImgUrlCrawled', {
                        detail: { url: nextUrl, previewUrl: previewUrl }
                    }));
                } catch (e) {
                    console.error('Error processing URL in queue:', nextUrl, e);
                }
            }
            this.queueTimeoutUid = setTimeout(() => {
                this.queueTimeoutUid = null;
                this.runQueuePreviewImgUrls();
            }, 0);
        }

        requestQueuePreviewImgUrl (url) {
            if (!url) {
                console.warn('Invalid URL requested for preview image:', url);
                return;
            }
            const cachedImgUrl = this.getCacheImgUrl(url);
            if (cachedImgUrl !== undefined) {
                console.debug('Using cached preview image URL:', url, cachedImgUrl);
                this.dispatchEvent(new CustomEvent('previewImgUrlCrawled', {
                    detail: { url: url, previewUrl: cachedImgUrl }
                }));
            } else {
                this.queuePreviewImgUrls.push(url);
                this.runQueuePreviewImgUrls();
            }
        }

        koneParseGalleryInfoList (list) {
            const resultGalleryInfo = [];
            for (const item of list) {
                const galleryInfo = {
                    link: item.querySelector('a')?.href || '',
                    badgeHtml: item.querySelector('a > div:nth-child(1) > div > div.items-stretch > span.justify-center.border')?.outerHTML || '',
                    title: item.querySelector('a > div:nth-child(1) > div > div.items-stretch > .text-ellipsis')?.innerHTML || '',
                    commentCountStr: item.querySelector('a > div:nth-child(1) > div > div.items-stretch > span.text-xs')?.innerHTML || '',
                    author: item.querySelector('a > div:nth-child(1) > div:nth-child(2)')?.innerHTML || '',
                    timeStr: item.querySelector('a > div:nth-child(1) > div:nth-child(3)')?.innerHTML || '',
                    viewStr: item.querySelector('a > div:nth-child(1) > div:nth-child(4)')?.innerHTML || '',
                    rating: parseInt(item.querySelector('a > div:nth-child(1) > div:nth-child(5)')?.innerHTML.replace(/[^0-9\-]/g, '')) || 0,
                    isRatingHigh: item.querySelector('a > div:nth-child(1) > div:nth-child(5)')?.classList.contains('text-red-500') || false,
                    isRatingLow: false,
                };
                if (galleryInfo.rating < 0) {
                    galleryInfo.isRatingLow = true;
                }
                resultGalleryInfo.push(galleryInfo);
            }
            return resultGalleryInfo;
        }

        buildGalleryCard (galleryInfo) {
            const card = document.createElement('a');
            card.href = galleryInfo.link;
            card.classList.add('kgv-gallery');
            if (galleryInfo.isRatingHigh) {
                card.classList.add('kgv-gallery-good');
            }
            if (galleryInfo.isRatingLow) {
                card.classList.add('kgv-gallery-bad');
            }
            card.innerHTML = /*html*/ `
<div class="kgv-gallery-preview">
    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 -960 960 960" fill="currentColor"><path d="M320-160h320v-120q0-66-47-113t-113-47q-66 0-113 47t-47 113v120Zm160-360q66 0 113-47t47-113v-120H320v120q0 66 47 113t113 47ZM160-80v-80h80v-120q0-61 28.5-114.5T348-480q-51-32-79.5-85.5T240-680v-120h-80v-80h640v80h-80v120q0 61-28.5 114.5T612-480q51 32 79.5 85.5T720-280v120h80v80H160Z"/></svg>
</div>
<div class="kgv-gallery-info">
    <div class="kgv-gallery-info-1">
        <span class="kgv-title">${galleryInfo.title}</span>
        <span class="kgv-comment">
            ${galleryInfo.commentCountStr}
        </span>
    </div>
    <div class="kgv-gallery-info-2">
        <span class="kgv-author">
            <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 -960 960 960" fill="currentColor"><path d="M480-480q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 66-47 113t-113 47ZM160-160v-112q0-34 17.5-62.5T224-378q62-31 126-46.5T480-440q66 0 130 15.5T736-378q29 15 46.5 43.5T800-272v112H160Zm80-80h480v-32q0-11-5.5-20T700-306q-54-27-109-40.5T480-360q-56 0-111 13.5T260-306q-9 5-14.5 14t-5.5 20v32Zm240-320q33 0 56.5-23.5T560-640q0-33-23.5-56.5T480-720q-33 0-56.5 23.5T400-640q0 33 23.5 56.5T480-560Zm0-80Zm0 400Z"/></svg>
            ${galleryInfo.author}
        </span>
    </div>
    <div class="kgv-gallery-info-3">
        <span class="kgv-category">
            ${galleryInfo.badgeHtml || ''}
        </span>    
        <span class="kgv-view">
            <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 512 512"><circle cx="256" cy="256" r="64" fill="currentColor"/><path fill="currentColor" d="M490.84 238.6c-26.46-40.92-60.79-75.68-99.27-100.53C349 110.55 302 96 255.66 96c-42.52 0-84.33 12.15-124.27 36.11c-40.73 24.43-77.63 60.12-109.68 106.07a31.92 31.92 0 0 0-.64 35.54c26.41 41.33 60.4 76.14 98.28 100.65C162 402 207.9 416 255.66 416c46.71 0 93.81-14.43 136.2-41.72c38.46-24.77 72.72-59.66 99.08-100.92a32.2 32.2 0 0 0-.1-34.76ZM256 352a96 96 0 1 1 96-96a96.11 96.11 0 0 1-96 96Z"/></svg>
            ${galleryInfo.viewStr}
        </span>
        <span class="kgv-vote">
            <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 512 512"><path fill="currentColor" d="M456 128a40 40 0 0 0-37.23 54.6l-84.17 84.17a39.86 39.86 0 0 0-29.2 0l-60.17-60.17a40 40 0 1 0-74.46 0L70.6 306.77a40 40 0 1 0 22.63 22.63L193.4 229.23a39.86 39.86 0 0 0 29.2 0l60.17 60.17a40 40 0 1 0 74.46 0l84.17-84.17A40 40 0 1 0 456 128Z"/></svg>
            ${galleryInfo.rating}
        </span>
    </div>
</div>
            `;
            const onCrawled = (e) => {
                if (Kgv.filterOnlyPathUrl(e.detail.url) === Kgv.filterOnlyPathUrl(galleryInfo.link)) {
                    this.removeEventListener('previewImgUrlCrawled', onCrawled);
                    const previewElement = card.querySelector('.kgv-gallery-preview');
                    if (previewElement) {
                        previewElement.innerHTML = '';
                        if (e.detail.previewUrl) {
                            const imgElement = document.createElement('img');
                            imgElement.loading = 'lazy';
                            imgElement.src = e.detail.previewUrl;
                            
                            previewElement.appendChild(imgElement);
                        }
                    }
                }
            }
            this.addEventListener('previewImgUrlCrawled', onCrawled);
            this.requestQueuePreviewImgUrl(galleryInfo.link);

            return card;
        }

        renderGalleryList () {
            const listContainer = document.querySelector('div.grow.flex.flex-col.overflow-hidden.relative > div.grow');
            const list = document.querySelectorAll('div.grow.flex.flex-col.overflow-hidden.relative > div.grow > div.w-full');
            if (!listContainer || !list) {
                console.warn('List container or list not found.');
                return;
            }
            const galleryInfoList = this.koneParseGalleryInfoList(list);
            if (galleryInfoList.length === 0) {
                console.warn('No gallery info found.');
                return;
            }
            
            if (this.galleryViewListElement) {
                this.galleryViewListElement.remove();
                this.galleryViewListElement = null;
            }
            this.galleryViewListElement = document.createElement('div');
            this.galleryViewListElement.classList.add('kgv-list');
            galleryInfoList.map(this.buildGalleryCard.bind(this)).forEach(card => {
                this.galleryViewListElement.appendChild(card);
            });

            listContainer.after(this.galleryViewListElement);
            listContainer.style.display = 'none';
        }

        observeURLChange() {
            let lastUrl = location.href;

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

            const urlChangeHandler = () => {
                if (location.href !== lastUrl && location.href.includes('/s/')) {
                    lastUrl = location.href;
                    onURLChange();
                }
            };

            const urlObserver = new MutationObserver(urlChangeHandler);
            urlObserver.observe(document.body, { childList: true, subtree: true });

            const originalPush = history.pushState;
            history.pushState = function () {
                originalPush.apply(this, arguments);
                urlChangeHandler();
            };

            window.addEventListener('popstate', urlChangeHandler);
            onURLChange(); // Initial call to render on script load
        }
    }

    // Initialize the Kgv instance
    const kgvInstance = await Kgv.getInstance();
    if (document.readyState === 'complete' || document.readyState === 'interactive') {
        kgvInstance.observeURLChange();
    } else {
        document.addEventListener('DOMContentLoaded', () => {
            kgvInstance.observeURLChange();
        });
    }
})();

QingJ © 2025

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