Gelbooru Enhanced Experience

Click on thumbnails to view full-size images/videos with favorite, pool, and navigation

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Gelbooru Enhanced Experience
// @namespace    http://tampermonkey.net/
// @version      11.0
// @description  Click on thumbnails to view full-size images/videos with favorite, pool, and navigation
// @author       kanrau
// @match        https://gelbooru.com/*
// @match        http://gelbooru.com/*
// @grant        none
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // ============ CONFIGURATION ============
    const CONFIG = {
        POOLS:  [
            { id: '69275', name: 'colored likes' },
            { id: '69675', name: 'Mehbooba' },
            { id: '69875', name: 'Butter' },
            { id: '70030', name: 'hinata' },
            { id: '70031', name: 'tatsumaki' }
        ],// change with your fav pools' id
        PRELOAD_COUNT: 3,
        MESSAGE_DURATION: 3000,
        TOAST_DURATION: 2000,
        POSTS_PER_PAGE: { favorites: 50, pool: 45, regular: 42 }
    };

    const COLORS = {
        bg: '#212121',
        bgLight: '#2f2f2f',
        bgDark: '#1a1a1a',
        text: '#c1c1c1',
        darktext: '#000000',
        link: '#4c98e7',
        linkHover: '#6db3f2',
        artist: '#f2a0a0',
        border: '#3a3a3a',
        success: 'rgba(76,175,80,. 9)',
        error: 'rgba(244,67,54,. 9)'
    };

    const STYLES = {
        link: `color: ${COLORS.link};text-decoration: none;font: 12px Verdana,sans-serif;cursor:pointer`,
        artist: `color:${COLORS.artist};text-decoration:none;font:12px Verdana,sans-serif;cursor: pointer`,
        separator: `color:${COLORS.text};font:12px Verdana,sans-serif`,
        overlay: `display:none;position: fixed;inset:0;background:rgba(0,0,0,. 85);z-index:999999;cursor:pointer;justify-content:center;align-items: center`,
        media: 'max-width:85vw;max-height:85vh;display:none',
        arrow: `position:fixed;top:50%;transform:translateY(-50%);font-size:60px;color:${COLORS.text};cursor:pointer;user-select:none;padding:20px;opacity:. 7;transition:opacity . 3s,transform .3s`,
        dropdown: `display:none;position:absolute;top:100%;left:0;background:${COLORS.bgLight};border:1px solid ${COLORS. border};z-index:1000002;min-width:150px;text-align:left;margin-top:2px;border-radius:3px`,
        dropdownItem: `display:block;padding: 8px 12px;color:${COLORS.link};font:12px Verdana,sans-serif;text-decoration:none;border-bottom:1px solid ${COLORS. border}`,
        topBar: `position:fixed;top: 15px;left:50%;transform:translateX(-50%);display:flex;gap: 5px;align-items:center;z-index:1000000;background:rgba(26,26,26,0.85);padding:8px 15px;border-radius:3px;border:1px solid ${COLORS.border}`
    };

    // ============ STATE ============
    const state = {
        currentIndex: -1,
        thumbnails: [],
        cache: {},
        overlayVisible: false,
        suspendMain: false
    };

    const elements = {};

    // ============ UTILITIES ============
    const utils = {
        isVideoFocused: () => document.activeElement === elements.video,
        isInputFocused: () => ['INPUT', 'TEXTAREA'].includes(document.activeElement?. tagName),
        getPostUrl: (id) => `https://gelbooru.com/index.php?page=post&s=view&id=${id}`,

        createElement(tag, styles, props = {}) {
            const el = document.createElement(tag);
            if (styles) el.style.cssText = styles;
            Object.assign(el, props);
            return el;
        },

        addHoverUnderline(el) {
            el.onmouseenter = () => { el.style.textDecoration = 'underline'; el.style.color = COLORS.linkHover; };
            el.onmouseleave = () => { el.style.textDecoration = 'none'; el.style.color = COLORS.link; };
        },

        addHoverUnderlineArtist(el) {
            el.onmouseenter = () => el.style.textDecoration = 'underline';
            el.onmouseleave = () => el.style.textDecoration = 'none';
        },

        addHoverScale(el) {
            el. onmouseenter = () => { el.style.opacity = '1'; el.style.transform = 'translateY(-50%) scale(1.2)'; };
            el.onmouseleave = () => { el.style. opacity = '. 7'; el.style.transform = 'translateY(-50%)'; };
        }
    };

    // ============ API ============
    const api = {
        async fetchPostData(postId) {
            try {
                const response = await fetch(utils.getPostUrl(postId), { credentials: 'include' });
                if (!response.ok) return null;

                const html = await response.text();
                if (html.includes('not available in your country')) return null;

                const doc = new DOMParser().parseFromString(html, 'text/html');

                let mediaUrl = null, isVideo = false;

                const img = doc.querySelector('#image');
                if (img?. src) {
                    mediaUrl = img. getAttribute('src');
                } else {
                    const video = doc. querySelector('video#gelcomVideoPlayer source');
                    if (video?. src) {
                        mediaUrl = video.getAttribute('src');
                        isVideo = true;
                    }
                }

                if (! mediaUrl) {
                    const match = html.match(/https?:\/\/[^"'\s]+\. gelbooru\.com\/images\/[^"'\s]+\.(jpg|jpeg|png|gif|webp|mp4|webm)/i);
                    if (match) {
                        mediaUrl = match[0];
                        isVideo = /\.(mp4|webm)$/i.test(mediaUrl);
                    }
                }

                const artistTag = doc.querySelector('li.tag-type-artist a[href*="tags="]');
                let artistName = null, artistUrl = null;
                if (artistTag) {
                    artistName = artistTag. textContent.trim();
                    artistUrl = artistTag.getAttribute('href');
                    if (artistUrl && ! artistUrl.startsWith('http')) {
                        artistUrl = 'https://gelbooru.com/' + artistUrl;
                    }
                }

                return { mediaUrl, isVideo, artistName, artistUrl };
            } catch (e) {
                console.error('Fetch error:', e);
                return null;
            }
        },

        async addToFavorite(postId) {
            try {
                await fetch(`https://gelbooru.com/index. php?page=post&s=vote&id=${postId}&type=up`, { credentials: 'include' });
                const res = await fetch(`https://gelbooru.com/public/addfav.php?id=${postId}`, { credentials: 'include' });
                return res.ok;
            } catch { return false; }
        },

        async addToPool(postId, poolId) {
            try {
                const body = new URLSearchParams({ id: poolId, commit: 'import', [`posts[${postId}]`]: '1' });
                const res = await fetch(`https://gelbooru.com/index.php?page=pool&s=import&id=${poolId}`, {
                    method:  'POST',
                    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                    body,
                    credentials:  'include'
                });
                return res.ok;
            } catch { return false; }
        },

        async removeFromFavorite(postId) {
            try {
                const res = await fetch(`https://gelbooru.com/index.php?page=favorites&s=delete&id=${postId}`, { credentials: 'include' });
                return res.ok;
            } catch { return false; }
        },

        async removeFromPool(postId, poolId) {
            try {
                const res = await fetch(`https://gelbooru.com/public/remove.php?removepool_post=1&pool_id=${poolId}&id=${postId}`, { credentials: 'include' });
                return res.ok;
            } catch { return false; }
        }
    };

    // ============ UI ============
    const ui = {
        showMessage(text, success = true) {
            const { message } = elements;
            message.textContent = text;
            message.style.background = success ? COLORS.success : COLORS.error;
            message.style.display = 'block';
            message.style.color = '#000000';
            setTimeout(() => message.style.display = 'none', CONFIG.MESSAGE_DURATION);
        },

        showToast(text, success = true) {
            document.getElementById('gel-toast')?.remove();
            const toast = utils.createElement('div', `
                position:fixed;top:15%;right:2%;padding:8px 15px;
                background:${success ? COLORS.bgLight : COLORS.error};
                color: ${COLORS.text};border: 1px solid ${COLORS.border};
                border-radius: 3px;z-index:10000;font: bold 12px Verdana,sans-serif
            `, { id: 'gel-toast', textContent: text });
            document.body. appendChild(toast);
            setTimeout(() => toast.remove(), CONFIG.TOAST_DURATION);
        },

        updateArtist(data) {
            const { artist } = elements;
            if (data?. artistName && data?. artistUrl) {
                artist.textContent = data.artistName;
                artist. href = data.artistUrl;
                artist. style.pointerEvents = 'auto';
            } else {
                artist. textContent = '(no artist)';
                artist.href = '#';
                artist. style.pointerEvents = 'none';
            }
        },

        createLink(text, onClick) {
            const link = utils.createElement('a', STYLES. link, { href: 'javascript:;', textContent: text });
            utils.addHoverUnderline(link);
            if (onClick) link.onclick = (e) => { e.preventDefault(); e.stopPropagation(); onClick(); };
            return link;
        },

        createPoolDropdown(postId, forOverlay = false) {
            const wrapper = utils.createElement('span', 'position:relative;display:inline-block');
            const trigger = utils.createElement('a', forOverlay ? STYLES.link : null, { href: 'javascript:;', innerHTML: 'Add to pool ▾' });
            if (forOverlay) utils.addHoverUnderline(trigger);

            const menu = utils.createElement('div', STYLES. dropdown);

            CONFIG.POOLS.forEach((pool, i) => {
                const item = utils. createElement('a', STYLES.dropdownItem, { href: 'javascript:;', textContent: pool. name });
                if (i === CONFIG.POOLS.length - 1) item.style.borderBottom = 'none';
                item.onmouseenter = () => { item.style.background = COLORS.link; item.style.color = '#fff'; };
                item.onmouseleave = () => { item.style.background = 'transparent'; item.style. color = COLORS. link; };
                item.onclick = async (e) => {
                    e.preventDefault();
                    e.stopPropagation();
                    menu.style.display = 'none';
                    const success = await api.addToPool(postId, pool.id);
                    forOverlay ? ui.showMessage(success ? `✓ Added to ${pool.name}` : '✗ Failed', success)
                               : ui.showToast(success ? `✓ Added to ${pool.name}` : '✗ Failed', success);
                };
                menu.appendChild(item);
            });

            trigger.onclick = (e) => { e.preventDefault(); e.stopPropagation(); menu.style.display = menu.style.display === 'none' ?  'block' :  'none'; };
            document.addEventListener('click', (e) => { if (! wrapper.contains(e. target)) menu.style.display = 'none'; });

            wrapper.append(trigger, menu);
            return { wrapper, menu };
        }
    };

    // ============ OVERLAY ============
    const overlay = {
        create() {
            const el = elements;

            el.overlay = utils.createElement('div', STYLES.overlay, { id: 'gel-enlarger-overlay' });
            el.loading = utils.createElement('div', `color:${COLORS. text};font:24px Verdana,sans-serif;position:absolute`, { textContent: 'Loading...' });

            el.image = utils.createElement('img', `${STYLES.media};object-fit:contain;cursor:pointer`);
            el.image.onclick = (e) => { e.stopPropagation(); this.openPost(); };

            el.videoContainer = utils.createElement('div', `${STYLES.media};position:relative;display:flex;align-items:center;justify-content:center`);
            el.video = utils.createElement('video', 'max-width:85vw;max-height:85vh;width:auto;height:auto', {
                id: 'gel-enlarger-video',
                controls: true,
                loop: true,
                autoplay: true
            });

            el.video.ondblclick = (e) => { e.preventDefault(); e.stopPropagation(); };

            let clickCount = 0, clickTimer = null;
            el.video.onclick = (e) => {
                e.stopPropagation();
                clickCount++;
                if (clickCount === 1) {
                    clickTimer = setTimeout(() => clickCount = 0, 300);
                } else if (clickCount === 2) {
                    clearTimeout(clickTimer);
                    clickCount = 0;
                    this.openPost();
                }
            };

            el.videoContainer.appendChild(el.video);

            el.counter = utils.createElement('div', `position:fixed;bottom:20px;left:50%;transform:translateX(-50%);color:${COLORS. text};font:12px Verdana,sans-serif;padding:5px 10px;background:${COLORS. bgDark};border-radius:3px;border:1px solid ${COLORS.border}`);
            el.message = utils.createElement('div', `position:fixed;top:60px;left:50%;transform: translateX(-50%);padding:10px 20px;border-radius:3px;font:12px Verdana,sans-serif;display:none;color:#fff;z-index:1000001`);

            el.leftArrow = this.createArrow('left', '❮', () => {
                if (state.currentIndex <= 0) {
                    const pid = parseInt(new URL(window.location.href).searchParams.get('pid') || '0');

                    if (pid > 0) {
                        ui.showMessage('Start of page. Loading previous page...', true);
                        this.goToPreviousPage()
                    } else {
                        ui.showMessage('This is the first page', false);
                    }
                } else {
                    this.navigate(-1);
                }
            });
            el.rightArrow = this.createArrow('right', '&#10095;', () => {
                if (state.currentIndex >= state.thumbnails.length - 1) {
                    ui.showMessage('End of page.  Loading next page...', true);
                    setTimeout(() => this.goToNextPage(), 100);
                } else {
                    this. navigate(1);
                }
            });

            const topBar = utils.createElement('div', STYLES.topBar);
            topBar.onclick = (e) => e.stopPropagation();

            el.artist = utils.createElement('a', STYLES.artist, { textContent: 'loading... ', target: '_blank' });
            utils.addHoverUnderlineArtist(el.artist);
            el.artist.onclick = (e) => e.stopPropagation();

            el.favLink = ui.createLink('Add to favorites', async () => {
                const thumb = state.thumbnails[state.currentIndex];
                if (! thumb) return;
                const success = await api.addToFavorite(thumb. postId);
                ui.showMessage(success ? '✓ Added to favorites!' : '✗ Failed', success);
            });

            el.unfavLink = ui.createLink('Unfavorite', async () => {
                const thumb = state.thumbnails[state.currentIndex];
                if (!thumb) return;
                const success = await api.removeFromFavorite(thumb.postId);
                ui.showMessage(success ? '✓ Removed from favorites!' : '✗ Failed', success);
            });

            el.unpoolLink = ui.createLink('Unpool', async () => {
                const thumb = state.thumbnails[state.currentIndex];
                if (!thumb) return;
                const pool = CONFIG.POOLS[0]; // Using first pool (colored likes - 69275)
                const success = await api.removeFromPool(thumb.postId, pool.id);
                ui.showMessage(success ? `✓ Removed from ${pool.name}` : '✗ Failed', success);
            });


            const { wrapper:  poolWrapper, menu: poolMenu } = ui.createPoolDropdown(null, true);
            el.poolWrapper = poolWrapper;
            el.poolMenu = poolMenu;

            const sep = () => utils.createElement('span', STYLES.separator, { textContent: ' | ' });
            topBar.append(el.artist, sep(), el.favLink, sep(), el.unfavLink, sep(), poolWrapper, sep(), el.unpoolLink);

            el.overlay.append(el.loading, el.image, el.videoContainer, el.counter, el. message, el.leftArrow, el. rightArrow, topBar);
            el.overlay.onclick = (e) => { if (e.target === el.overlay) this.hide(); };
            el.overlay.addEventListener('click', () => el.poolMenu.style.display = 'none');

            document.body.appendChild(el.overlay);
            document.addEventListener('keydown', (e) => this.handleKeyboard(e), true);
        },

        createArrow(side, html, onClick) {
            const arrow = utils.createElement('div', `${STYLES.arrow};${side}:20px`, { innerHTML: html });
            utils.addHoverScale(arrow);
            arrow.onclick = (e) => { e.stopPropagation(); onClick(); };
            return arrow;
        },

        show() {
            elements.overlay.style. display = 'flex';
            state.overlayVisible = true;
        },

        hide() {
            const el = elements;
            el.overlay.style.display = 'none';
            el.image.src = '';
            el.image.style.display = 'none';
            el.video.pause();
            el.video.src = '';
            el.videoContainer.style.display = 'none';
            el.loading.style.display = 'block';
            el. loading.textContent = 'Loading...';
            el.poolMenu.style.display = 'none';
            state.currentIndex = -1;
            state.overlayVisible = false;
        },

        navigate(dir) {
            const newIndex = state.currentIndex + dir;
            if (newIndex >= 0 && newIndex < state.thumbnails. length) {
                this.loadMedia(newIndex);
            }
        },

        goToNextPage() {
            const currentUrl = window.location.href;
            const url = new URL(currentUrl);
            if (currentUrl.includes('page=post') && currentUrl.includes('s=view')) {
                ui.showMessage('No next page on single post view', false);
                return;
            }
            const pid = parseInt(url.searchParams.get('pid') || '0');
            const temp = url.searchParams.get('page');
            const perPage = temp === 'favorites' ? CONFIG.POSTS_PER_PAGE.favorites : temp === 'pool' ? CONFIG.POSTS_PER_PAGE.pool :CONFIG.POSTS_PER_PAGE.regular;
            if (state.thumbnails.length < perPage) {
                ui.showMessage('This is the last page', false);
                return;
            }
            url.searchParams.set('pid', pid + perPage);
            sessionStorage.setItem('gelbooru_overlay_nav', 'next');
            window.location.href = url.toString();
        },

        goToPreviousPage() {
            const currentUrl = window.location.href;
            const url = new URL(currentUrl);

            if (currentUrl.includes('page=post') && currentUrl.includes('s=view')) {
                ui.showMessage('No previous page on single post view', false);
                return;
            }

            const pid = parseInt(url.searchParams.get('pid') || '0');
            const temp = url.searchParams.get('page');

            const perPage = temp === 'favorites'
            ? CONFIG.POSTS_PER_PAGE.favorites
            : temp === 'pool'
            ? CONFIG.POSTS_PER_PAGE.pool
            : CONFIG.POSTS_PER_PAGE.regular;

            if (pid <= 0) {
                ui.showMessage('This is the first page', false);
                return;
            }

            url.searchParams.set('pid', Math.max(0, pid - perPage));

            // Save state for restoring overlay
            sessionStorage.setItem('gelbooru_overlay_nav', 'prev');

            window.location.href = url.toString();
        },

        openPost() {
            const thumb = state.thumbnails[state.currentIndex];
            if (thumb) window.open(thumb.postUrl, '_blank');
        },

        handleKeyboard(e) {
            if (! state.overlayVisible) return;
            // if (state.suspendMain) return;
            if (e.key === ' ' && utils.isVideoFocused()) return;

            const actions = {
                'Escape': () => this.hide(),
                'ArrowLeft': () => {
                    if (state.currentIndex <= 0) {
                        const pid = parseInt(new URL(window.location.href).searchParams.get('pid') || '0');

                        if (pid > 0) {
                            ui.showMessage('Start of page. Loading previous page...', true);
                            setTimeout(() => this.goToPreviousPage(), 500);
                        } else {
                            ui.showMessage('This is the first page', false);
                        }
                    } else {
                        this.navigate(-1);
                    }
                },
                'ArrowRight': () => {
                    if (state.currentIndex >= state.thumbnails. length - 1) {
                        ui.showMessage('End of page. Loading next page...', true);
                        setTimeout(() => this.goToNextPage(), 1000);
                    } else {
                        this.navigate(1);
                    }
                },
                'f': () => elements.favLink.click(),
                'F': () => elements.favLink.click(),
                'j': () => this.addToFirstPool(),
                'J': () => this.addToFirstPool(),
                ' ': () => {
                    if (elements.videoContainer.style.display !== 'none' && ! utils.isVideoFocused()) {
                        elements.video.paused ?  elements.video.play() : elements.video.pause();
                    }
                }
            };

            if (actions[e.key]) {
                e.preventDefault();
                e.stopPropagation();
                e.stopImmediatePropagation();
                actions[e.key]();
            }
        },

        async addToFirstPool() {
            const thumb = state.thumbnails[state.currentIndex];
            if (!thumb) return;
            const pool = CONFIG.POOLS[0];
            const success = await api. addToPool(thumb.postId, pool.id);
            ui.showMessage(success ? `✓ Added to ${pool.name}` : '✗ Failed', success);
        },

        async loadMedia(index) {
            if (index < 0 || index >= state.thumbnails.length) return;

            state.currentIndex = index;
            const thumb = state.thumbnails[index];
            const el = elements;

            el.counter.textContent = `${index + 1} / ${state.thumbnails.length}`;
            const isFirst = index === 0;
            const canGoPrevPage = new URL(window.location.href).searchParams.get('pid') > 0;

            el.leftArrow.style.opacity = (index > 0 || canGoPrevPage) ? '.7' : '.3';
            el.leftArrow.style.cursor = (index > 0 || canGoPrevPage) ? 'pointer' : 'default';
            el. loading.textContent = 'Loading...';
            el.loading.style. display = 'block';
            el.image.style.display = 'none';
            el. videoContainer.style.display = 'none';
            el.video. pause();
            el.poolMenu.style.display = 'none';
            el.artist.textContent = 'loading...';
            el.artist.href = '#';
            el.artist.style.pointerEvents = 'none';

            this.updatePoolDropdown(thumb. postId);

            let data = state.cache[thumb. postId];
            if (!data) {
                data = await api.fetchPostData(thumb. postId);
                if (data) state.cache[thumb.postId] = data;
            }

            if (data?. mediaUrl) {
                this.displayMedia(data);
                ui.updateArtist(data);
                this.preloadNext(index);
            } else {
                el. loading.textContent = 'Failed to load media';
                el.artist.textContent = '(error)';
            }
        },

        updatePoolDropdown(postId) {
            const { poolMenu } = elements;
            poolMenu.querySelectorAll('a').forEach((item, i) => {
                const pool = CONFIG.POOLS[i];
                item.onclick = async (e) => {
                    e. preventDefault();
                    e.stopPropagation();
                    poolMenu.style.display = 'none';
                    const success = await api.addToPool(postId, pool.id);
                    ui. showMessage(success ?  `✓ Added to ${pool.name}` : '✗ Failed', success);
                };
            });
        },

        displayMedia(data) {
            const el = elements;
            if (data.isVideo) {
                el.video.src = data.mediaUrl;
                el. videoContainer.style.display = 'block';
                el.loading.style.display = 'none';
                el.video. blur();
                el. video.play().catch(() => {});
            } else {
                const img = new Image();
                img.onload = () => {
                    el. image.src = data.mediaUrl;
                    el.image.style.display = 'block';
                    el.loading.style.display = 'none';
                };
                img.onerror = () => el.loading.textContent = 'Failed to load image';
                img.src = data.mediaUrl;
            }
        },

        preloadNext(fromIndex) {
            for (let i = 1; i <= CONFIG. PRELOAD_COUNT; i++) {
                const idx = fromIndex + i;
                if (idx >= state.thumbnails. length) break;

                const thumb = state.thumbnails[idx];
                if (state.cache[thumb. postId]) continue;

                api.fetchPostData(thumb.postId).then(data => {
                    if (data) {
                        state. cache[thumb.postId] = data;
                        if (! data.isVideo && data.mediaUrl) new Image().src = data.mediaUrl;
                    }
                });
            }
        }
    };

    // ============ THUMBNAILS ============
    const thumbnails = {
        collect() {
            state.thumbnails = [];
            document.querySelectorAll('img[src*="/thumbnails/"]').forEach(img => {
                const link = img.closest('a[href*="page=post"][href*="s=view"][href*="id="]');
                if (! link) return;

                const match = link.href. match(/[?&]id=(\d+)/);
                if (! match) return;

                state.thumbnails.push({
                    img,
                    link,
                    postId: match[1],
                    postUrl: link.href
                });
            });
            return state.thumbnails. length;
        },

        setup() {
            this.collect();
            state.thumbnails.forEach((thumb) => {
                if (thumb.link. dataset.enlargerSetup) return;
                thumb.link.dataset.enlargerSetup = 'true';

                thumb.img.style.cursor = 'zoom-in';
                thumb.img.title = 'Click to enlarge (Ctrl+Click for original)';

                thumb.link.addEventListener('click', (e) => {
                    if (e.ctrlKey || e.metaKey || e.button === 1) return;

                    e.preventDefault();
                    e.stopPropagation();
                    e.stopImmediatePropagation();

                    this.collect();
                    const idx = state.thumbnails.findIndex(t => t.postId === thumb.postId);
                    if (idx === -1) return;

                    overlay.show();
                    overlay.loadMedia(idx);
                }, true);
            });
        }
    };

    // ============ SINGLE POST PAGE ============
    const singlePost = {
        init() {
            const postId = new URLSearchParams(window. location.search).get('id');
            if (!postId) return;

            this.injectPoolDropdown(postId);
            this.setupKeyboardShortcuts(postId);
        },

        injectPoolDropdown(postId) {
            // Find the "Leave a Comment" link
            const commentLink = document.querySelector('a#showCommentBox');

            if (commentLink) {
                const { wrapper } = ui.createPoolDropdown(postId, false);

                // Insert separator and pool dropdown after favorite link
                const sep = document.createTextNode(' | ');

                if (commentLink.nextSibling) {
                    commentLink.parentNode.insertBefore(sep, commentLink.nextSibling);
                    commentLink.parentNode.insertBefore(wrapper, sep. nextSibling);
                } else {
                    commentLink.parentNode. appendChild(sep);
                    commentLink. parentNode.appendChild(wrapper);
                }
                return;
            }
        },

        setupKeyboardShortcuts(postId) {
            document.addEventListener('keydown', async (e) => {
                if (state.overlayVisible) return;
                if (utils.isInputFocused()) return;

                // F - Trigger favorite
                if (e.key === 'f' || e.key === 'F') {
                    const favLink = document. querySelector('a[onclick*="addFav"]');
                    if (favLink) {
                        e.preventDefault();
                        favLink.click();
                        ui.showToast('✓ Favorites toggled');
                    }
                }

                // J - Add to first pool
                if (e.key === 'j' || e.key === 'J') {
                    e.preventDefault();
                    const pool = CONFIG.POOLS[0];
                    ui.showToast(`Adding to ${pool.name}...`);
                    const success = await api. addToPool(postId, pool.id);
                    ui.showToast(success ? `✓ Added to ${pool.name}` : '✗ Failed', success);
                }

                // 1-9 - Add to pool by number
                const num = parseInt(e. key);
                if (num >= 1 && num <= CONFIG.POOLS.length) {
                    e.preventDefault();
                    const pool = CONFIG.POOLS[num - 1];
                    ui. showToast(`Adding to ${pool.name}...`);
                    const success = await api.addToPool(postId, pool.id);
                    ui.showToast(success ?  `✓ Added to ${pool.name}` : '✗ Failed', success);
                }
            });
        }
    };


    const poolQuickDelete = {
        POOL_ID: "69275",

        init() {
            const url = new URL(window.location.href);
            if (
                url.searchParams.get("page") !== "pool" ||
                url.searchParams.get("s") !== "show" ||
                url.searchParams.get("id") !== this.POOL_ID
            ) return;

            const tryInject = () => {
                const label = document.querySelector('label[for="del-mode"]');
                if (!label) return false;
                if (document.getElementById("gel-pool-quick-delete")) return true;

                const box = document.createElement("div");
                box.id = "gel-pool-quick-delete";
                box.style.cssText = `
                margin-top:8px;
                display:flex;
                gap:6px;
                align-items:center;
                font:12px Verdana,sans-serif;
            `;

                const input = document.createElement("input");
                input.type = "text";
                input.placeholder = "Post ID to remove";
                input.style.cssText = `
                padding:4px 6px;
                background:#1a1a1a;
                color:#c1c1c1;
                border:1px solid #3a3a3a;
                border-radius:3px;
                width:140px;
            `;

                const btn = document.createElement("button");
                btn.textContent = "Delete";
                btn.style.cssText = `
                padding:4px 10px;
                background:#2f2f2f;
                color:#c1c1c1;
                border:1px solid #3a3a3a;
                border-radius:3px;
                cursor:pointer;
            `;

                btn.onmouseenter = () => btn.style.background = "#3a3a3a";
                btn.onmouseleave = () => btn.style.background = "#2f2f2f";

                btn.onclick = async () => {
                    const id = input.value.trim();
                    if (!/^\d+$/.test(id)) {
                        ui.showToast("✗ Invalid post id", false);
                        return;
                    }

                    ui.showToast("Removing...");

                    try {
                        const res = await fetch(
                            `https://gelbooru.com/public/remove.php?removepool_post=1&pool_id=${this.POOL_ID}&id=${id}`,
                            { credentials: "include" }
                        );

                        ui.showToast(res.ok ? "✓ Removed from pool" : "✗ Failed", res.ok);
                    } catch {
                        ui.showToast("✗ Failed", false);
                    }
                };

                box.append(input, btn);
                label.insertAdjacentElement("afterend", box);

                return true;
            };

            if (tryInject()) return;

            const obs = new MutationObserver(() => {
                if (tryInject()) obs.disconnect();
            });

            obs.observe(document.body, { childList: true, subtree: true });
        }
    };


    const homeLinks = {
        inject() {
            const tryInject = () => {
                const linksDiv = document.querySelector('div#links.space');
                if (!linksDiv) return false;

                if (document.getElementById("gel-custom-links")) return true;

                const wrapper = document.createElement("span");
                wrapper.id = "gel-custom-links";

                const sampleLink = document.querySelector('a');
                const linkColor = sampleLink ? getComputedStyle(sampleLink).color : 'inherit';
                const poolLink = document.createElement("a");
                poolLink.style.color = linkColor;
                poolLink.href = "https://gelbooru.com/index.php?page=pool&s=show&id=69275";
                poolLink.textContent = "My Pool";

                poolLink.title = "Open my pool";

                const favLink = document.createElement("a");
                favLink.href = "https://gelbooru.com/index.php?page=favorites&s=view&id=1420596";
                favLink.textContent = "My Favorites";
                favLink.style.color = linkColor;
                favLink.title = "Open my favorites";

                wrapper.append(" ");
                wrapper.appendChild(poolLink);
                wrapper.append(" ");
                wrapper.appendChild(favLink);

                linksDiv.appendChild(wrapper);
                return true;
            };

            // Try immediately
            if (tryInject()) return;

            // Otherwise observe until it appears
            const obs = new MutationObserver(() => {
                if (tryInject()) obs.disconnect();
            });

            obs.observe(document.body, { childList: true, subtree: true });
        }
    };

    // ============ INIT ============
    function init() {

        homeLinks.inject();
        poolQuickDelete.init();
        const url = window.location.href;
        const isSinglePost = url. includes('page=post') && url.includes('s=view') && url.includes('id=');

        if (isSinglePost) {
            singlePost. init();
        }



        overlay.create();
        thumbnails.setup();
        // Restore overlay state after page navigation
        const navState = sessionStorage.getItem('gelbooru_overlay_nav');

        if (navState) {
            sessionStorage.removeItem('gelbooru_overlay_nav');

            setTimeout(() => {
                if (state.thumbnails.length > 0) {
                    overlay.show();

                    if (navState === 'next') {
                        overlay.loadMedia(0); // first image
                    } else if (navState === 'prev') {
                        overlay.loadMedia(state.thumbnails.length - 1); // last image
                    }
                }
            }, 100);
        }
        new MutationObserver(() => thumbnails.setup()).observe(document.body, { childList: true, subtree: true });

    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();