Gelbooru Enhanced Experience

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

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey, Greasemonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

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

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

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