Adds direct image links, and displays Google Images results in an in-page gallery.
// ==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();
}
})();