Google Image Gallery

Adds direct image links, and displays Google Images results in an in-page gallery.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Google Image Gallery
// @namespace    https://greasyfork.org/
// @version      1.0
// @author       Bui Quoc Dung
// @description  Adds direct image links, and displays Google Images results in an in-page gallery.
// @include      /^https:\/\/www\.google\.(co\.)?[a-z]{2,3}\/search.*(tbm=isch|udm=2)/
// @include      /^https:\/\/www\.google\.com(\.[a-z]{2,3})?\/search.*(tbm=isch|udm=2)/
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @connect      *
// @run-at       document-start
// ==/UserScript==

(() => {
    "use strict";

    const siteConfig = {
        selectors: {
            resultContainer: "#rso div[data-q]",
            imageWrapper: "div[data-snm] div[data-img-wrapper]",
            thumbnail: "img",
            jsdataHost: "[jsdata]",
            jsController: "div[jscontroller]",
            inlineScript: "script:not([src])",
            panel: ".panel_gidlp"
        },

        attributes: {
            jsdata: "jsdata",
            originalWidth: "ow",
            originalHeight: "oh",
            availability: "availability",
            thumbnailUrl: "thumbnailUrl"
        },

        classes: {
            processed: "paneled_gidlp",
            panel: "panel_gidlp",
            waiting: "waiting_gidlp",
            checking: "checking_gidlp",
            available: "available_gidlp",
            unavailable: "unavailable_gidlp",

            gallery: "gallery_gidlp",
            galleryVisible: "gallery_visible_gidlp",
            sidebar: "gallery_sidebar_gidlp",
            sidebarHeader: "gallery_sidebar_header_gidlp",
            sidebarList: "gallery_sidebar_list_gidlp",
            thumbnailItem: "gallery_thumbnail_item_gidlp",
            thumbnailActive: "gallery_thumbnail_active_gidlp",
            thumbnailUnavailable: "gallery_thumbnail_unavailable_gidlp",
            thumbnailImage: "gallery_thumbnail_image_gidlp",
            thumbnailInfo: "gallery_thumbnail_info_gidlp",
            thumbnailIndex: "gallery_thumbnail_index_gidlp",
            thumbnailDimension: "gallery_thumbnail_dimension_gidlp",

            viewer: "gallery_viewer_gidlp",
            imageStage: "gallery_image_stage_gidlp",
            mainImage: "gallery_main_image_gidlp",
            imageLoaded: "gallery_image_loaded_gidlp",
            imageZoomed: "gallery_image_zoomed_gidlp",
            loading: "gallery_loading_gidlp",
            error: "gallery_error_gidlp",

            navigationButton: "gallery_navigation_button_gidlp",
            previousButton: "gallery_previous_button_gidlp",
            nextButton: "gallery_next_button_gidlp",
            closeButton: "gallery_close_button_gidlp",
            counter: "gallery_counter_gidlp"
        },

        request: {
            searchPath: "/search?",
            timeout: 10000,
            maxConcurrent: 6,
            retryDelay: 100,
            processDelay: 500
        },

        intersection: {
            root: null,
            rootMargin: "400px 0px",
            threshold: 0
        },

        messages: {
            waiting: "Waiting to check image availability",
            checking: "Checking image availability",
            available: "Open image gallery",
            unavailable: "This image may no longer be available",
            loading: "Loading image...",
            error: "Unable to display this image",
            galleryTitle: "Google Images",
            previous: "Previous image",
            next: "Next image",
            close: "Close gallery"
        },

        dataReaders: {
            initial(script) {
                const match = script.textContent.match(
                    /var [a-z]=(\{".*?\});/
                );

                if (!match) {
                    return null;
                }

                try {
                    return JSON.parse(match[1]);
                } catch {
                    return null;
                }
            },

            cached(wrapper, cache) {
                const jsdata = wrapper
                    .closest(siteConfig.selectors.jsdataHost)
                    ?.getAttribute(siteConfig.attributes.jsdata);

                const key = jsdata?.match(/[^;]+$/)?.[0];

                return key
                    ? cache?.[key]?.[1]?.[3]
                    : null;
            },

            controller(wrapper) {
                return wrapper
                    .closest(siteConfig.selectors.jsController)
                    ?.__jscontroller
                    ?.pending
                    ?.value
                    ?.Vz
                    ?.Bp
                    ?.[1]
                    ?.Bp
                    ?.[3];
            }
        }
    };

    const styles = `
${siteConfig.selectors.resultContainer}
${siteConfig.selectors.imageWrapper} > .${siteConfig.classes.panel} {
    position: absolute;
    right: 0;
    bottom: 0;
    z-index: 1;
    font-size: 10pt;
    line-height: normal;
    opacity: 1;
    transition:
        opacity 0.2s ease,
        filter 0.2s ease;
}

${siteConfig.selectors.resultContainer}
${siteConfig.selectors.imageWrapper} > .${siteConfig.classes.panel}::before {
    display: block;
    padding: 0 0.3ex;
    border-radius: 0.2em;
    background-color: #444;
    color: #fff;
    content:
        attr(data-${siteConfig.attributes.originalWidth})
        "x"
        attr(data-${siteConfig.attributes.originalHeight});
}

${siteConfig.selectors.resultContainer}
${siteConfig.selectors.imageWrapper} > .${siteConfig.classes.panel}:visited::before {
    color: #f9f;
}

${siteConfig.selectors.resultContainer}
${siteConfig.selectors.imageWrapper} > .${siteConfig.classes.waiting} {
    opacity: 1;
}

${siteConfig.selectors.resultContainer}
${siteConfig.selectors.imageWrapper} > .${siteConfig.classes.checking} {
    opacity: 0.72;
}

${siteConfig.selectors.resultContainer}
${siteConfig.selectors.imageWrapper} > .${siteConfig.classes.available} {
    opacity: 1;
}

${siteConfig.selectors.resultContainer}
${siteConfig.selectors.imageWrapper} > .${siteConfig.classes.unavailable} {
    opacity: 0.42;
    filter: grayscale(1);
}

${siteConfig.selectors.resultContainer}
${siteConfig.selectors.imageWrapper} > .${siteConfig.classes.unavailable}::before {
    text-decoration: line-through;
}

.${siteConfig.classes.gallery} {
    position: fixed;
    inset: 0;
    z-index: 2147483646;
    display: grid;
    grid-template-columns: 260px minmax(0, 1fr);
    visibility: hidden;
    opacity: 0;
    pointer-events: none;
    background-color: transparent;
    transition:
        opacity 0.2s ease,
        visibility 0.2s ease;
}

.${siteConfig.classes.gallery}.${siteConfig.classes.galleryVisible} {
    visibility: visible;
    opacity: 1;
    pointer-events: auto;
}

.${siteConfig.classes.sidebar} {
    display: flex;
    flex-direction: column;
    min-width: 0;
    height: 100vh;
    overflow: hidden;
    background-color: rgba(18, 18, 18, 0.97);
    border-right: 1px solid rgba(255, 255, 255, 0.16);
    box-shadow: 8px 0 28px rgba(0, 0, 0, 0.4);
}

.${siteConfig.classes.sidebarHeader} {
    flex: 0 0 auto;
    padding: 18px 16px;
    color: #fff;
    border-bottom: 1px solid rgba(255, 255, 255, 0.13);
    font: 600 18px/1.3 Arial, sans-serif;
}

.${siteConfig.classes.sidebarList} {
    flex: 1 1 auto;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 12px;
    min-height: 0;
    padding: 10px;
    overflow-x: hidden;
    overflow-y: auto;
    scrollbar-width: thin;
    scrollbar-color:
        rgba(255, 255, 255, 0.45)
        rgba(255, 255, 255, 0.08);
}

.${siteConfig.classes.sidebarList}::-webkit-scrollbar {
    width: 10px;
}

.${siteConfig.classes.sidebarList}::-webkit-scrollbar-track {
    background-color: rgba(255, 255, 255, 0.07);
}

.${siteConfig.classes.sidebarList}::-webkit-scrollbar-thumb {
    border: 2px solid transparent;
    border-radius: 999px;
    background-color: rgba(255, 255, 255, 0.42);
    background-clip: padding-box;
}

.${siteConfig.classes.thumbnailItem} {
    position: relative;
    display: block;
    flex: 0 0 auto;
    width: 100%;
    min-width: 0;
    padding: 0;
    overflow: hidden;
    border: 2px solid transparent;
    border-radius: 12px;
    background-color: rgba(255, 255, 255, 0.07);
    cursor: pointer;
    text-align: left;
    transition:
        border-color 0.15s ease,
        background-color 0.15s ease,
        opacity 0.15s ease,
        transform 0.15s ease;
}

.${siteConfig.classes.thumbnailItem}:hover {
    background-color: rgba(255, 255, 255, 0.14);
    transform: scale(1.025);
}

.${siteConfig.classes.thumbnailItem}.${siteConfig.classes.thumbnailActive} {
    border-color: #8ab4f8;
    box-shadow:
        0 0 0 1px rgba(138, 180, 248, 0.4),
        0 3px 14px rgba(0, 0, 0, 0.35);
}

.${siteConfig.classes.thumbnailItem}.${siteConfig.classes.thumbnailUnavailable} {
    opacity: 0.38;
    filter: grayscale(1);
}

.${siteConfig.classes.thumbnailImage} {
    display: block;
    width: 100%;
    height: 100%;
    min-height: 90px;
    max-height: 260px;
    border-radius: 10px;
    object-fit: contain;
    background-color: rgba(255, 255, 255, 0.08);
}

.${siteConfig.classes.thumbnailInfo} {
    position: absolute;
    left: 5px;
    right: 5px;
    bottom: 5px;
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    pointer-events: none;
}

.${siteConfig.classes.thumbnailIndex},
.${siteConfig.classes.thumbnailDimension} {
    display: inline-block;
    max-width: 100%;
    padding: 3px 6px;
    overflow: hidden;
    border-radius: 4px;
    color: #f1f3f4;
    background-color: rgba(0, 0, 0, 0.68);
    font: 600 10px/1.2 Arial, sans-serif;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.${siteConfig.classes.thumbnailIndex} {
    flex: 0 0 auto;
}

.${siteConfig.classes.thumbnailDimension} {
    flex: 0 1 auto;
}

.${siteConfig.classes.thumbnailItem}.${siteConfig.classes.thumbnailUnavailable}
.${siteConfig.classes.thumbnailIndex},
.${siteConfig.classes.thumbnailItem}.${siteConfig.classes.thumbnailUnavailable}
.${siteConfig.classes.thumbnailDimension} {
    opacity: 0.55;
    text-decoration: line-through;
}

.${siteConfig.classes.viewer} {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    min-width: 0;
    height: 100vh;
    overflow: hidden;
    background-color: rgba(0, 0, 0, 0.58);
    backdrop-filter: blur(2px);
    -webkit-backdrop-filter: blur(2px);
}

.${siteConfig.classes.imageStage} {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    width: calc(100% - 150px);
    height: calc(100% - 72px);
    overflow: auto;
}

.${siteConfig.classes.mainImage} {
    display: block;
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
    opacity: 0;
    cursor: zoom-in;
    user-select: none;
    filter: drop-shadow(0 12px 40px rgba(0, 0, 0, 0.55));
    transition: opacity 0.2s ease;
}

.${siteConfig.classes.mainImage}.${siteConfig.classes.imageLoaded} {
    opacity: 1;
}

.${siteConfig.classes.mainImage}.${siteConfig.classes.imageZoomed} {
    max-width: none;
    max-height: none;
    cursor: zoom-out;
}

.${siteConfig.classes.loading},
.${siteConfig.classes.error} {
    position: absolute;
    max-width: 320px;
    padding: 12px 16px;
    border-radius: 8px;
    color: #fff;
    background-color: rgba(0, 0, 0, 0.7);
    font: 14px/1.4 Arial, sans-serif;
    text-align: center;
    pointer-events: none;
}

.${siteConfig.classes.error} {
    display: none;
}

.${siteConfig.classes.navigationButton},
.${siteConfig.classes.closeButton} {
    position: absolute;
    z-index: 2;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    border: 1px solid rgba(255, 255, 255, 0.12);
    color: #fff;
    background-color: rgba(0, 0, 0, 0.45);
    box-shadow: 0 4px 18px rgba(0, 0, 0, 0.28);
    cursor: pointer;
    backdrop-filter: blur(4px);
    -webkit-backdrop-filter: blur(4px);
    transition:
        background-color 0.15s ease,
        opacity 0.15s ease,
        transform 0.15s ease;
}

.${siteConfig.classes.navigationButton}:hover,
.${siteConfig.classes.closeButton}:hover {
    background-color: rgba(0, 0, 0, 0.78);
}

.${siteConfig.classes.navigationButton} {
    top: 50%;
    width: 48px;
    height: 72px;
    border-radius: 12px;
    font: 34px/1 Arial, sans-serif;
    transform: translateY(-50%);
}

.${siteConfig.classes.navigationButton}:active {
    transform: translateY(-50%) scale(0.94);
}

.${siteConfig.classes.previousButton} {
    left: 16px;
}

.${siteConfig.classes.nextButton} {
    right: 16px;
}

.${siteConfig.classes.closeButton} {
    top: 16px;
    right: 16px;
    width: 42px;
    height: 42px;
    border-radius: 50%;
    font: 27px/1 Arial, sans-serif;
}

.${siteConfig.classes.closeButton}:active {
    transform: scale(0.94);
}

.${siteConfig.classes.counter} {
    position: absolute;
    left: 50%;
    bottom: 15px;
    z-index: 2;
    padding: 6px 11px;
    border-radius: 999px;
    color: #fff;
    background-color: rgba(0, 0, 0, 0.55);
    font: 12px/1.2 Arial, sans-serif;
    backdrop-filter: blur(4px);
    -webkit-backdrop-filter: blur(4px);
    transform: translateX(-50%);
}

@media (max-width: 900px) {
    .${siteConfig.classes.gallery} {
        grid-template-columns: 210px minmax(0, 1fr);
    }
}

@media (max-width: 620px) {
    .${siteConfig.classes.gallery} {
        grid-template-columns: 110px minmax(0, 1fr);
    }

    .${siteConfig.classes.sidebarHeader} {
        padding: 14px 10px;
        font-size: 14px;
    }

    .${siteConfig.classes.sidebarList} {
        gap: 8px;
        padding: 7px;
    }

    .${siteConfig.classes.thumbnailInfo} {
        right: 3px;
        bottom: 3px;
        padding: 2px 4px;
        font-size: 9px;
    }

    .${siteConfig.classes.imageStage} {
        width: calc(100% - 92px);
    }

    .${siteConfig.classes.navigationButton} {
        width: 38px;
        height: 58px;
    }

    .${siteConfig.classes.previousButton} {
        left: 7px;
    }

    .${siteConfig.classes.nextButton} {
        right: 7px;
    }
}
`;

    const availabilityCache = new Map();
    const pendingUrls = new Set();
    const checkQueue = [];

    let resultContainer = null;
    let imageDataCache = null;
    let activeChecks = 0;
    let processTimer = null;
    let galleryItems = [];
    let currentGalleryIndex = -1;
    let bodyOverflowBeforeGallery = "";

    GM_addStyle(styles);

    const visibilityObserver = new IntersectionObserver(
        handleVisibilityChanges,
        siteConfig.intersection
    );

    const gallery = document.createElement("div");
    const gallerySidebar = document.createElement("aside");
    const sidebarHeader = document.createElement("div");
    const sidebarList = document.createElement("div");
    const galleryViewer = document.createElement("main");
    const imageStage = document.createElement("div");
    const mainImage = document.createElement("img");
    const loadingMessage = document.createElement("div");
    const errorMessage = document.createElement("div");
    const previousButton = document.createElement("button");
    const nextButton = document.createElement("button");
    const closeButton = document.createElement("button");
    const counter = document.createElement("div");

    gallery.className = siteConfig.classes.gallery;
    gallerySidebar.className = siteConfig.classes.sidebar;
    sidebarHeader.className = siteConfig.classes.sidebarHeader;
    sidebarList.className = siteConfig.classes.sidebarList;
    galleryViewer.className = siteConfig.classes.viewer;
    imageStage.className = siteConfig.classes.imageStage;
    mainImage.className = siteConfig.classes.mainImage;
    loadingMessage.className = siteConfig.classes.loading;
    errorMessage.className = siteConfig.classes.error;

    previousButton.className =
        `${siteConfig.classes.navigationButton} ` +
        siteConfig.classes.previousButton;

    nextButton.className =
        `${siteConfig.classes.navigationButton} ` +
        siteConfig.classes.nextButton;

    closeButton.className = siteConfig.classes.closeButton;
    counter.className = siteConfig.classes.counter;

    sidebarHeader.textContent = siteConfig.messages.galleryTitle;
    loadingMessage.textContent = siteConfig.messages.loading;
    errorMessage.textContent = siteConfig.messages.error;

    previousButton.type = "button";
    nextButton.type = "button";
    closeButton.type = "button";

    previousButton.textContent = "‹";
    nextButton.textContent = "›";
    closeButton.textContent = "×";

    previousButton.title = siteConfig.messages.previous;
    nextButton.title = siteConfig.messages.next;
    closeButton.title = siteConfig.messages.close;

    previousButton.setAttribute(
        "aria-label",
        siteConfig.messages.previous
    );

    nextButton.setAttribute(
        "aria-label",
        siteConfig.messages.next
    );

    closeButton.setAttribute(
        "aria-label",
        siteConfig.messages.close
    );

    imageStage.append(
        mainImage,
        loadingMessage,
        errorMessage
    );

    gallerySidebar.append(
        sidebarHeader,
        sidebarList
    );

    galleryViewer.append(
        imageStage,
        previousButton,
        nextButton,
        closeButton,
        counter
    );

    gallery.append(
        gallerySidebar,
        galleryViewer
    );

    function mountGallery() {
        if (document.body && !gallery.isConnected) {
            document.body.append(gallery);
        }
    }

    function isValidImageUrl(url) {
        if (!url) {
            return false;
        }

        try {
            const parsedUrl = new URL(url, location.href);

            return (
                parsedUrl.protocol === "http:" ||
                parsedUrl.protocol === "https:"
            );
        } catch {
            return false;
        }
    }

    function getLinkUrl(link) {
        return link.getAttribute("href") || "";
    }

    function shouldUseNativeLinkBehavior(event) {
        return (
            event.button !== 0 ||
            event.ctrlKey ||
            event.metaKey ||
            event.shiftKey ||
            event.altKey
        );
    }

    function getThumbnailUrl(wrapper) {
        if (!wrapper) {
            return "";
        }

        const images = Array.from(
            wrapper.querySelectorAll(
                siteConfig.selectors.thumbnail
            )
        );

        const candidates = images
            .map(image => {
                const url =
                    image.currentSrc ||
                    image.getAttribute("src") ||
                    image.getAttribute("data-src") ||
                    image.getAttribute("data-iurl") ||
                    "";

                const rect =
                    image.getBoundingClientRect();

                const style =
                    getComputedStyle(image);

                return {
                    image,
                    url,
                    visible:
                        style.display !== "none" &&
                        style.visibility !== "hidden" &&
                        Number(style.opacity) > 0 &&
                        rect.width > 1 &&
                        rect.height > 1,
                    loaded:
                        image.naturalWidth > 1 ||
                        image.naturalHeight > 1,
                    area:
                        Math.max(rect.width, 0) *
                        Math.max(rect.height, 0)
                };
            })
            .filter(candidate =>
                isValidImageUrl(candidate.url)
            )
            .sort((a, b) => {
                if (a.visible !== b.visible) {
                    return Number(b.visible) -
                        Number(a.visible);
                }

                if (a.loaded !== b.loaded) {
                    return Number(b.loaded) -
                        Number(a.loaded);
                }

                return b.area - a.area;
            });

        return candidates[0]?.url || "";
    }

    function getCurrentThumbnailUrl(link) {
        const wrapper = link.closest(
            siteConfig.selectors.imageWrapper
        );

        const freshThumbnailUrl =
            getThumbnailUrl(wrapper);

        if (freshThumbnailUrl) {
            link.dataset[
                siteConfig.attributes.thumbnailUrl
            ] = freshThumbnailUrl;
        }

        return (
            freshThumbnailUrl ||
            link.dataset[
                siteConfig.attributes.thumbnailUrl
            ] ||
            ""
        );
    }

    function setAvailability(link, state) {
        link.classList.remove(
            siteConfig.classes.waiting,
            siteConfig.classes.checking,
            siteConfig.classes.available,
            siteConfig.classes.unavailable
        );

        link.classList.add(siteConfig.classes[state]);

        link.dataset[
            siteConfig.attributes.availability
        ] = state;

        link.title = siteConfig.messages[state];
    }

    function applyAvailabilityToMatchingLinks(
        url,
        available
    ) {
        if (!resultContainer) {
            return;
        }

        resultContainer
            .querySelectorAll(siteConfig.selectors.panel)
            .forEach(link => {
                if (getLinkUrl(link) !== url) {
                    return;
                }

                visibilityObserver.unobserve(link);

                setAvailability(
                    link,
                    available
                        ? "available"
                        : "unavailable"
                );
            });

        galleryItems.forEach(item => {
            if (item.url === url) {
                item.available = available;
            }
        });

        sidebarList
            .querySelectorAll(
                `.${siteConfig.classes.thumbnailItem}`
            )
            .forEach((thumbnail, index) => {
                const item = galleryItems[index];

                if (!item || item.url !== url) {
                    return;
                }

                thumbnail.classList.toggle(
                    siteConfig.classes.thumbnailUnavailable,
                    !available
                );
            });
    }

    function finishAvailabilityCheck(url, available) {
        availabilityCache.set(url, available);
        pendingUrls.delete(url);

        applyAvailabilityToMatchingLinks(
            url,
            available
        );

        activeChecks = Math.max(
            0,
            activeChecks - 1
        );

        runCheckQueue();
    }

    function parseContentType(responseHeaders) {
        return responseHeaders
            ?.match(/^content-type:\s*([^\r\n]+)/im)?.[1]
            ?.trim()
            ?.toLowerCase() || "";
    }

    function isImageResponse(response) {
        const statusValid =
            response.status >= 200 &&
            response.status < 400;

        if (!statusValid) {
            return false;
        }

        const contentType = parseContentType(
            response.responseHeaders
        );

        return (
            !contentType ||
            contentType.startsWith("image/") ||
            contentType.includes("octet-stream")
        );
    }

    function checkUrl(url) {
        GM_xmlhttpRequest({
            method: "GET",
            url,

            headers: {
                Range: "bytes=0-0"
            },

            responseType: "blob",
            timeout: siteConfig.request.timeout,
            anonymous: true,

            onload(response) {
                finishAvailabilityCheck(
                    url,
                    isImageResponse(response)
                );
            },

            onerror() {
                finishAvailabilityCheck(url, false);
            },

            ontimeout() {
                finishAvailabilityCheck(url, false);
            },

            onabort() {
                finishAvailabilityCheck(url, false);
            }
        });
    }

    function runCheckQueue() {
        while (
            activeChecks <
                siteConfig.request.maxConcurrent &&
            checkQueue.length > 0
        ) {
            const url = checkQueue.shift();

            if (availabilityCache.has(url)) {
                pendingUrls.delete(url);

                applyAvailabilityToMatchingLinks(
                    url,
                    availabilityCache.get(url)
                );

                continue;
            }

            activeChecks++;
            checkUrl(url);
        }
    }

    function queueAvailabilityCheck(link) {
        const url = getLinkUrl(link);

        if (!isValidImageUrl(url)) {
            visibilityObserver.unobserve(link);
            setAvailability(link, "unavailable");
            return;
        }

        if (availabilityCache.has(url)) {
            visibilityObserver.unobserve(link);

            setAvailability(
                link,
                availabilityCache.get(url)
                    ? "available"
                    : "unavailable"
            );

            return;
        }

        setAvailability(link, "checking");

        if (pendingUrls.has(url)) {
            return;
        }

        pendingUrls.add(url);
        checkQueue.push(url);
        runCheckQueue();
    }

    function handleVisibilityChanges(entries) {
        entries.forEach(entry => {
            if (!entry.isIntersecting) {
                return;
            }

            visibilityObserver.unobserve(entry.target);
            queueAvailabilityCheck(entry.target);
        });
    }

    function observeAvailability(link) {
        const url = getLinkUrl(link);

        if (!isValidImageUrl(url)) {
            visibilityObserver.unobserve(link);
            setAvailability(link, "unavailable");
            return;
        }

        if (availabilityCache.has(url)) {
            visibilityObserver.unobserve(link);

            setAvailability(
                link,
                availabilityCache.get(url)
                    ? "available"
                    : "unavailable"
            );

            return;
        }

        setAvailability(link, "waiting");
        visibilityObserver.observe(link);
    }

    function collectGalleryItems() {
        if (!resultContainer) {
            return [];
        }

        const seenUrls = new Set();
        const items = [];

        resultContainer
            .querySelectorAll(siteConfig.selectors.panel)
            .forEach(link => {
                const url = getLinkUrl(link);

                if (
                    !isValidImageUrl(url) ||
                    seenUrls.has(url)
                ) {
                    return;
                }

                seenUrls.add(url);

                items.push({
                    url,
                    thumbnailUrl:
                        getCurrentThumbnailUrl(link),
                    width:
                        link.dataset[
                            siteConfig.attributes.originalWidth
                        ] || "???",
                    height:
                        link.dataset[
                            siteConfig.attributes.originalHeight
                        ] || "???",
                    available:
                        availabilityCache.get(url),
                    link
                });
            });

        return items;
    }

    function createThumbnailItem(item, index) {
        const button = document.createElement("button");
        const image = document.createElement("img");
        const info = document.createElement("span");
        const indexBadge = document.createElement("span");
        const dimensionBadge = document.createElement("span");

        button.type = "button";
        button.className =
            siteConfig.classes.thumbnailItem;

        button.title =
            `${index + 1}. ${item.width} × ${item.height}`;

        image.className =
            siteConfig.classes.thumbnailImage;

        info.className =
            siteConfig.classes.thumbnailInfo;

        indexBadge.className =
            siteConfig.classes.thumbnailIndex;

        dimensionBadge.className =
            siteConfig.classes.thumbnailDimension;

        image.alt = "";
        image.loading = "lazy";
        image.draggable = false;

        if (item.thumbnailUrl) {
            image.src = item.thumbnailUrl;
        }

        image.addEventListener("error", () => {
            const freshThumbnailUrl =
                getCurrentThumbnailUrl(item.link);

            if (
                freshThumbnailUrl &&
                freshThumbnailUrl !==
                    image.getAttribute("src")
            ) {
                image.src = freshThumbnailUrl;
            }
        });

        const numericWidth = Number(item.width);
        const numericHeight = Number(item.height);

        if (
            Number.isFinite(numericWidth) &&
            Number.isFinite(numericHeight) &&
            numericWidth > 0 &&
            numericHeight > 0
        ) {
            button.style.aspectRatio =
                `${numericWidth} / ${numericHeight}`;
        }

        indexBadge.textContent =
            String(index + 1);

        dimensionBadge.textContent =
            `${item.width} × ${item.height}`;

        info.append(
            indexBadge,
            dimensionBadge
        );

        if (index === currentGalleryIndex) {
            button.classList.add(
                siteConfig.classes.thumbnailActive
            );
        }

        if (item.available === false) {
            button.classList.add(
                siteConfig.classes.thumbnailUnavailable
            );
        }

        button.addEventListener("click", () => {
            showGalleryItem(index);
        });

        button.append(image, info);

        return button;
    }

    function renderGallerySidebar() {
        sidebarList.replaceChildren();

        galleryItems.forEach((item, index) => {
            sidebarList.append(
                createThumbnailItem(item, index)
            );
        });
    }

    function refreshGalleryItems() {
        const currentUrl =
            galleryItems[currentGalleryIndex]?.url || "";

        galleryItems = collectGalleryItems();

        if (currentUrl) {
            const newIndex = galleryItems.findIndex(
                item => item.url === currentUrl
            );

            if (newIndex >= 0) {
                currentGalleryIndex = newIndex;
            }
        }

        renderGallerySidebar();
        updateGalleryCounter();
        scrollActiveThumbnailIntoView();
    }

    function updateGalleryCounter() {
        if (
            currentGalleryIndex < 0 ||
            !galleryItems.length
        ) {
            counter.textContent = "0 / 0";
            return;
        }

        counter.textContent =
            `${currentGalleryIndex + 1} / ` +
            `${galleryItems.length}`;
    }

    function updateActiveThumbnail() {
        sidebarList
            .querySelectorAll(
                `.${siteConfig.classes.thumbnailItem}`
            )
            .forEach((item, index) => {
                item.classList.toggle(
                    siteConfig.classes.thumbnailActive,
                    index === currentGalleryIndex
                );
            });

        scrollActiveThumbnailIntoView();
    }

    function scrollActiveThumbnailIntoView() {
        const activeThumbnail =
            sidebarList.querySelector(
                `.${siteConfig.classes.thumbnailActive}`
            );

        activeThumbnail?.scrollIntoView({
            block: "nearest",
            behavior: "smooth"
        });
    }

    function showGalleryItem(index) {
        if (!galleryItems.length) {
            return;
        }

        const normalizedIndex =
            (index + galleryItems.length) %
            galleryItems.length;

        const item = galleryItems[normalizedIndex];

        currentGalleryIndex = normalizedIndex;

        mainImage.classList.remove(
            siteConfig.classes.imageLoaded,
            siteConfig.classes.imageZoomed
        );

        loadingMessage.style.display = "";
        errorMessage.style.display = "none";

        mainImage.removeAttribute("src");

        requestAnimationFrame(() => {
            mainImage.src = item.url;
        });

        updateGalleryCounter();
        updateActiveThumbnail();
    }

    function showPreviousImage() {
        if (currentGalleryIndex <= 0) {
            return;
        }

        showGalleryItem(currentGalleryIndex - 1);
    }

    function showNextImage() {
        if (
            currentGalleryIndex >=
            galleryItems.length - 1
        ) {
            return;
        }

        showGalleryItem(currentGalleryIndex + 1);
    }

    function openGallery(url) {
        mountGallery();

        galleryItems = collectGalleryItems();

        currentGalleryIndex = galleryItems.findIndex(
            item => item.url === url
        );

        if (currentGalleryIndex < 0) {
            return;
        }

        renderGallerySidebar();

        bodyOverflowBeforeGallery =
            document.body.style.overflow;

        document.body.style.overflow = "hidden";

        gallery.classList.add(
            siteConfig.classes.galleryVisible
        );

        showGalleryItem(currentGalleryIndex);

        document.addEventListener(
            "keydown",
            handleGalleryKeydown
        );
    }

    function closeGallery() {
        gallery.classList.remove(
            siteConfig.classes.galleryVisible
        );

        mainImage.classList.remove(
            siteConfig.classes.imageLoaded,
            siteConfig.classes.imageZoomed
        );

        mainImage.removeAttribute("src");

        galleryItems = [];
        currentGalleryIndex = -1;
        sidebarList.replaceChildren();

        document.body.style.overflow =
            bodyOverflowBeforeGallery;

        document.removeEventListener(
            "keydown",
            handleGalleryKeydown
        );
    }

    function handleGalleryKeydown(event) {
        if (event.key === "Escape") {
            event.preventDefault();
            closeGallery();
            return;
        }

        if (event.key === "ArrowLeft") {
            event.preventDefault();
            showPreviousImage();
            return;
        }

        if (event.key === "ArrowRight") {
            event.preventDefault();
            showNextImage();
            return;
        }

        if (event.key === "Home") {
            event.preventDefault();
            showGalleryItem(0);
            return;
        }

        if (event.key === "End") {
            event.preventDefault();
            showGalleryItem(
                galleryItems.length - 1
            );
        }
    }

    function handlePanelClick(event) {
        const link = event.currentTarget;
        const url = getLinkUrl(link);

        event.stopImmediatePropagation();
        event.stopPropagation();

        if (!isValidImageUrl(url)) {
            event.preventDefault();
            return;
        }

        if (shouldUseNativeLinkBehavior(event)) {
            return;
        }

        event.preventDefault();
        openGallery(url);
    }

    function configureLink(
        link,
        imageData,
        wrapper
    ) {
        const imageUrl = imageData?.[0];
        const previousUrl = getLinkUrl(link);

        link.rel = "nofollow noopener noreferrer";

        link.dataset[
            siteConfig.attributes.originalWidth
        ] = imageData?.[2] || "???";

        link.dataset[
            siteConfig.attributes.originalHeight
        ] = imageData?.[1] || "???";

        link.dataset[
            siteConfig.attributes.thumbnailUrl
        ] = getThumbnailUrl(wrapper);

        if (isValidImageUrl(imageUrl)) {
            if (previousUrl !== imageUrl) {
                visibilityObserver.unobserve(link);

                link.classList.remove(
                    siteConfig.classes.waiting,
                    siteConfig.classes.checking,
                    siteConfig.classes.available,
                    siteConfig.classes.unavailable
                );

                delete link.dataset[
                    siteConfig.attributes.availability
                ];
            }

            link.href = imageUrl;
            observeAvailability(link);
        } else {
            visibilityObserver.unobserve(link);
            link.removeAttribute("href");
            setAvailability(link, "unavailable");
        }

        if (!link.dataset.clickHandlerAdded) {
            link.dataset.clickHandlerAdded = "true";

            link.addEventListener(
                "click",
                handlePanelClick,
                true
            );
        }
    }

    function createPanel(wrapper) {
        let link = wrapper.querySelector(
            siteConfig.selectors.panel
        );

        if (link) {
            return link;
        }

        link = document.createElement("a");
        link.className = siteConfig.classes.panel;

        wrapper.insertBefore(
            link,
            wrapper.firstChild
        );

        return link;
    }

    function processCachedEntry(wrapper) {
        const imageData =
            siteConfig.dataReaders.cached(
                wrapper,
                imageDataCache
            );

        wrapper.classList.add(
            siteConfig.classes.processed
        );

        const link = createPanel(wrapper);

        configureLink(
            link,
            imageData,
            wrapper
        );
    }

    function waitForControllerData(wrapper) {
        if (!wrapper.isConnected) {
            return;
        }

        const imageData =
            siteConfig.dataReaders.controller(
                wrapper
            );

        if (imageData) {
            wrapper.classList.add(
                siteConfig.classes.processed
            );

            const link = createPanel(wrapper);

            configureLink(
                link,
                imageData,
                wrapper
            );

            return;
        }

        setTimeout(
            waitForControllerData,
            siteConfig.request.retryDelay,
            wrapper
        );
    }

    function processEntries() {
        if (!resultContainer) {
            return;
        }

        const selector =
            `${siteConfig.selectors.imageWrapper}` +
            `:not(.${siteConfig.classes.processed})`;

        resultContainer
            .querySelectorAll(selector)
            .forEach(wrapper => {
                if (imageDataCache) {
                    processCachedEntry(wrapper);
                } else {
                    waitForControllerData(wrapper);
                }
            });

        if (
            gallery.classList.contains(
                siteConfig.classes.galleryVisible
            )
        ) {
            refreshGalleryItems();
        }
    }

    function scheduleEntryProcessing() {
        clearTimeout(processTimer);

        processTimer = setTimeout(
            processEntries,
            siteConfig.request.processDelay
        );
    }

    function interceptSearchResponses() {
        const originalOpen =
            XMLHttpRequest.prototype.open;

        XMLHttpRequest.prototype.open =
            function(method, url) {
                if (
                    typeof url === "string" &&
                    url.startsWith(
                        siteConfig.request.searchPath
                    )
                ) {
                    this.addEventListener(
                        "load",
                        () => {
                            const responseParts =
                                this.responseText.split(";[");

                            if (!responseParts.length) {
                                return;
                            }

                            try {
                                const responseData =
                                    JSON.parse(
                                        `[${responseParts.at(-1)}`
                                    )[0];

                                if (!imageDataCache) {
                                    imageDataCache = {};
                                }

                                responseData.forEach(
                                    item => {
                                        try {
                                            imageDataCache[
                                                item[0]
                                            ] = JSON.parse(
                                                item[1]
                                            );
                                        } catch {}
                                    }
                                );

                                scheduleEntryProcessing();
                            } catch {}
                        }
                    );
                }

                return originalOpen.apply(
                    this,
                    arguments
                );
            };
    }

    function readInitialImageData() {
        document
            .querySelectorAll(
                siteConfig.selectors.inlineScript
            )
            .forEach(script => {
                if (imageDataCache) {
                    return;
                }

                imageDataCache =
                    siteConfig.dataReaders.initial(
                        script
                    );
            });
    }

    function observeResults() {
        resultContainer =
            document.querySelector(
                siteConfig.selectors.resultContainer
            );

        if (!resultContainer) {
            return;
        }

        const mutationObserver =
            new MutationObserver(
                scheduleEntryProcessing
            );

        mutationObserver.observe(
            resultContainer,
            {
                childList: true,
                subtree: true
            }
        );

        processEntries();
    }

    function initialize() {
        mountGallery();
        readInitialImageData();
        observeResults();
    }

    previousButton.addEventListener(
        "click",
        event => {
            event.stopPropagation();
            showPreviousImage();
        }
    );

    nextButton.addEventListener(
        "click",
        event => {
            event.stopPropagation();
            showNextImage();
        }
    );

    closeButton.addEventListener(
        "click",
        event => {
            event.stopPropagation();
            closeGallery();
        }
    );

    mainImage.addEventListener(
        "click",
        event => {
            event.stopPropagation();

            mainImage.classList.toggle(
                siteConfig.classes.imageZoomed
            );
        }
    );

    mainImage.addEventListener(
        "load",
        () => {
            loadingMessage.style.display = "none";
            errorMessage.style.display = "none";

            mainImage.classList.add(
                siteConfig.classes.imageLoaded
            );
        }
    );

    mainImage.addEventListener(
        "error",
        () => {
            loadingMessage.style.display = "none";
            errorMessage.style.display = "block";

            mainImage.classList.remove(
                siteConfig.classes.imageLoaded
            );
        }
    );

    interceptSearchResponses();

    if (document.readyState === "loading") {
        addEventListener(
            "DOMContentLoaded",
            initialize,
            { once: true }
        );
    } else {
        initialize();
    }
})();