Better Thumbnail preview for drunkenslug xxx with infinite scrolling, Hover zoom, gallery view, Ctrl-hover lightbox, click-to-add-to-cart, configurable keyword filters, and session-based new post highlighting
// ==UserScript==
// @name better_drunkenslug_xxx_previews
// @namespace http://tampermonkey.net/
// @version 2.10.11
// @description Better Thumbnail preview for drunkenslug xxx with infinite scrolling, Hover zoom, gallery view, Ctrl-hover lightbox, click-to-add-to-cart, configurable keyword filters, and session-based new post highlighting
// @author takuto
// @match https://drunkenslug.com/*
// @run-at document-idle
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_xmlhttpRequest
// @connect *
// @license MIT
// ==/UserScript==
const EBDS_IS_SEARCH_PAGE = location.pathname.startsWith('/search');
const EBDS_ENABLED = (() => {
try {
const t = parseInt(new URLSearchParams(location.search).get('t'), 10);
return (Number.isInteger(t) && t >= 6000 && t <= 6999) || EBDS_IS_SEARCH_PAGE;
} catch (e) { return false; }
})();
const EBDS_DEFAULT_BLACKLIST_TERMS = [
'a.b.multimedia.erotica.male'
];
const EBDS_BLACKLIST_STORAGE_KEY = 'ebdsBlacklistTerms';
const EBDS_HIDE_WITHOUT_PICS_STORAGE_KEY = 'ebdsHideWithoutPictures';
const EBDS_MIN_FILE_SIZE_MB_KEY = 'ebdsMinFileSizeMb';
const EBDS_SCALE_HOVER_PREVIEW_KEY = 'ebdsScaleHoverPreview';
const EBDS_FILTER_LINKS_POSITION_KEY = 'ebdsFilterLinksPosition';
const EBDS_INFINITE_SCROLL_KEY = 'ebdsInfiniteScroll';
// Storage key for session expiration minutes setting
const EBDS_SESSION_EXPIRATION_KEY = 'ebdsSessionExpiration';
const EBDS_SAB_URL_KEY = 'ebdsSabnzbdUrl';
const EBDS_SAB_API_KEY = 'ebdsSabnzbdApiKey';
const EBDS_HIDE_SAB_BUTTON_KEY = 'ebdsHideSabReadFeedsButton';
let EBDS_HIDE_WITHOUT_PICTURES = true;
try {
const stored = localStorage.getItem(EBDS_HIDE_WITHOUT_PICS_STORAGE_KEY);
if (stored !== null) EBDS_HIDE_WITHOUT_PICTURES = (stored === '1');
} catch (e) { }
let EBDS_MIN_FILE_SIZE_MB = 0;
try {
const stored = Number(localStorage.getItem(EBDS_MIN_FILE_SIZE_MB_KEY));
if (Number.isFinite(stored) && stored >= 0) EBDS_MIN_FILE_SIZE_MB = stored;
} catch (e) { }
let EBDS_SCALE_HOVER_PREVIEW = false;
try {
EBDS_SCALE_HOVER_PREVIEW = localStorage.getItem(EBDS_SCALE_HOVER_PREVIEW_KEY) === '1';
} catch (e) { }
let EBDS_FILTER_LINKS_POSITION = 'footer';
try {
const storedPosition = localStorage.getItem(EBDS_FILTER_LINKS_POSITION_KEY);
if (['footer', 'header', 'hidden'].includes(storedPosition)) EBDS_FILTER_LINKS_POSITION = storedPosition;
} catch (e) { }
let EBDS_INFINITE_SCROLL = true;
try {
const stored = localStorage.getItem(EBDS_INFINITE_SCROLL_KEY);
if (stored !== null) EBDS_INFINITE_SCROLL = stored === '1';
} catch (e) { }
let EBDS_HIDE_SAB_BUTTON = false;
try {
EBDS_HIDE_SAB_BUTTON = localStorage.getItem(EBDS_HIDE_SAB_BUTTON_KEY) === '1';
} catch (e) { }
const EBDS_TEMPORARY_FILTER_BYPASS = {
withoutPictures: false,
keywords: false,
minimumSize: false
};
function parseFileSizeMb(value) {
const match = String(value || '').match(/\b(\d+(?:[.,]\d+)?)\s*(KB|MB|GB|TB)\b/i);
if (!match) return null;
const amount = Number(match[1].replace(',', '.'));
if (!Number.isFinite(amount)) return null;
const factors = { KB: 1 / 1024, MB: 1, GB: 1024, TB: 1024 * 1024 };
return amount * factors[match[2].toUpperCase()];
}
function applyCtrlHoverPreviewSizing(image) {
image.style.objectFit = 'contain';
if (!EBDS_SCALE_HOVER_PREVIEW) {
image.style.width = 'auto';
image.style.height = 'auto';
image.style.maxWidth = '95vw';
image.style.maxHeight = '95vh';
return;
}
image.style.maxWidth = '100vw';
image.style.maxHeight = '100vh';
const scaleToViewport = () => {
if (!image.naturalWidth || !image.naturalHeight) return;
const imageRatio = image.naturalWidth / image.naturalHeight;
const viewportRatio = window.innerWidth / window.innerHeight;
if (imageRatio >= viewportRatio) {
image.style.width = '100vw';
image.style.height = 'auto';
} else {
image.style.width = 'auto';
image.style.height = '100vh';
}
};
if (image.complete) scaleToViewport();
else image.addEventListener('load', scaleToViewport, { once: true });
}
function normalizeBlacklistTerm(term) {
return (term || '').trim().toLowerCase();
}
function getReleaseTitleText(row) {
const titleLink = row ? row.querySelector('a.title') : null;
return titleLink ? titleLink.textContent.trim().toLowerCase() : '';
}
function loadBlacklistTerms() {
try {
const raw = localStorage.getItem(EBDS_BLACKLIST_STORAGE_KEY);
if (raw !== null) {
const stored = JSON.parse(raw);
if (!Array.isArray(stored)) throw new Error('Invalid blacklist storage');
return Array.from(new Set(stored.map(normalizeBlacklistTerm).filter(Boolean)));
}
} catch (e) { }
return EBDS_DEFAULT_BLACKLIST_TERMS.slice();
}
function persistBlacklistTerms(terms) {
try { localStorage.setItem(EBDS_BLACKLIST_STORAGE_KEY, JSON.stringify(terms)); } catch (e) { }
}
let EBDS_BLACKLIST_TERMS = loadBlacklistTerms();
let ctrlPressed = false;
// Hidden prefixes (session-persistent; cleared on new session)
const EBDS_HIDDEN_PREFIXES_KEY = 'ebdsHiddenPrefixes';
const EBDS_CLICKED_STORAGE_KEY = 'ebdsClickedGuids';
const EBDS_UI_SESSION_KEY = 'ebdsUiSession';
let EBDS_HIDDEN_PREFIXES = new Set();
let EBDS_CLICKED_GUIDS = new Set();
const EBDS_PENDING_CART_REQUESTS = new Map();
function loadHiddenPrefixes() {
try {
const raw = localStorage.getItem(EBDS_HIDDEN_PREFIXES_KEY) || '[]';
const arr = JSON.parse(raw);
if (Array.isArray(arr)) return new Set(arr.filter(Boolean).map(s => String(s).toLowerCase()));
} catch (e) { }
return new Set();
}
function persistHiddenPrefixes() {
try { localStorage.setItem(EBDS_HIDDEN_PREFIXES_KEY, JSON.stringify(Array.from(EBDS_HIDDEN_PREFIXES))); } catch (e) { }
}
function loadClickedGuids() {
try {
const raw = localStorage.getItem(EBDS_CLICKED_STORAGE_KEY) || '[]';
const arr = JSON.parse(raw);
if (Array.isArray(arr)) return new Set(arr.filter(Boolean).map(String));
} catch (e) { }
return new Set();
}
function persistClickedGuids() {
try { localStorage.setItem(EBDS_CLICKED_STORAGE_KEY, JSON.stringify(Array.from(EBDS_CLICKED_GUIDS))); } catch (e) { }
}
function addClickedGuid(guid) {
if (!guid) return;
EBDS_CLICKED_GUIDS.add(String(guid));
persistClickedGuids();
}
function clearClickedGuids() {
EBDS_CLICKED_GUIDS = new Set();
try { localStorage.removeItem(EBDS_CLICKED_STORAGE_KEY); } catch (e) { }
}
EBDS_HIDDEN_PREFIXES = loadHiddenPrefixes();
EBDS_CLICKED_GUIDS = loadClickedGuids();
function derivePrefixFromSrc(src) {
try {
if (!src) return null;
const parts = src.split('/');
const filename = parts[parts.length - 1] || src;
const idx = Math.min(...['_', '&', '.'].map(c => {
const i = filename.indexOf(c);
return i === -1 ? Infinity : i;
}));
if (!Number.isFinite(idx) || idx <= 0) return filename.toLowerCase();
return filename.substring(0, idx).toLowerCase();
} catch (e) { return null; }
}
// Derive prefix from an element (prefer title/alt/dataset.name, fallback to src/href filename)
function derivePrefixFromElement(el) {
try {
if (!el) return null;
let candidate = null;
// Prefer title or alt or dataset.name
try { candidate = (el.getAttribute && el.getAttribute('title')) || el.alt || (el.dataset && el.dataset.name) || null; } catch (e) { }
// If candidate looks like the "posted • name • size" format, try to pick the token that contains underscores or looks like a filename
if (candidate) {
// split by '•' and pick token that contains '_' or '.' or '&', else pick the longest
const parts = candidate.split('•').map(s => s.trim()).filter(Boolean);
let pick = parts.find(p => /[_&.]/.test(p));
if (!pick) pick = parts.sort((a,b) => b.length - a.length)[0];
candidate = pick || candidate;
}
// If still no candidate or candidate seems not useful, fall back to src or dataset.href
if (!candidate) {
try { candidate = (el.dataset && el.dataset.href) || el.src || ''; } catch (e) { candidate = ''; }
}
if (!candidate) return null;
// If it's a URL, take last pathname segment
try { if (candidate.indexOf('/') !== -1) candidate = candidate.split('/').pop(); } catch (e) { }
candidate = candidate.split('?')[0].split('#')[0].trim();
// Derive prefix up to first delimiter
const idx = Math.min(...['_', '&', '.'].map(c => {
const i = candidate.indexOf(c);
return i === -1 ? Infinity : i;
}));
if (!Number.isFinite(idx) || idx <= 0) return candidate.toLowerCase();
return candidate.substring(0, idx).toLowerCase();
} catch (e) { return null; }
}
function matchesHiddenPrefixForSrc(src) {
try {
const p = derivePrefixFromSrc(src);
return p && EBDS_HIDDEN_PREFIXES && EBDS_HIDDEN_PREFIXES.has(p);
} catch (e) { return false; }
}
function matchesHiddenPrefixForRow(row) {
try {
if (!row) return false;
// Prefer checking an actual image element inside the row (title/alt/dataset may be present)
const img = row.querySelector('img');
if (img) {
const p = derivePrefixFromElement(img);
return p && EBDS_HIDDEN_PREFIXES && EBDS_HIDDEN_PREFIXES.has(p);
}
const anchor = row.querySelector('a[href]');
const href = anchor ? anchor.href : null;
if (!href) return false;
const p = derivePrefixFromSrc(href);
return p && EBDS_HIDDEN_PREFIXES && EBDS_HIDDEN_PREFIXES.has(p);
} catch (e) { return false; }
}
function addHiddenPrefix(pref) {
try {
if (!pref) return;
const p = String(pref).toLowerCase();
if (EBDS_HIDDEN_PREFIXES.has(p)) return;
EBDS_HIDDEN_PREFIXES.add(p);
persistHiddenPrefixes();
applyRowFiltering();
} catch (e) { }
}
function removeHiddenPrefix(pref) {
try {
if (!pref) return;
const p = String(pref).toLowerCase();
if (EBDS_HIDDEN_PREFIXES.has(p)) {
EBDS_HIDDEN_PREFIXES.delete(p);
persistHiddenPrefixes();
applyRowFiltering();
}
} catch (e) { }
}
function clearHiddenPrefixes() {
try {
EBDS_HIDDEN_PREFIXES = new Set();
localStorage.removeItem(EBDS_HIDDEN_PREFIXES_KEY);
} catch (e) { }
}
// Expose a few helpers globally
try { window.addHiddenPrefix = addHiddenPrefix; window.removeHiddenPrefix = removeHiddenPrefix; window.clearHiddenPrefixes = clearHiddenPrefixes; window.loadHiddenPrefixes = loadHiddenPrefixes; } catch (e) { }
function addToCartByGuid(guid, src) {
if (!guid) return Promise.resolve(false);
const guidKey = String(guid);
if (EBDS_PENDING_CART_REQUESTS.has(guidKey)) return EBDS_PENDING_CART_REQUESTS.get(guidKey);
const row = document.getElementById('guid' + guidKey);
const cart = row ? row.querySelector('.icon_cart') : null;
let request;
// Dynamically appended cart icons do not have the site's jQuery handler, so use the API fallback.
if (cart && !(row && row.dataset.ebdsInfiniteAdded === '1')) {
request = new Promise(resolve => {
let settled = false;
const observer = new MutationObserver(() => {
if (cart.classList.contains('icon_cart_clicked')) finish(true);
});
const timeout = setTimeout(() => finish(false), 10000);
function finish(added) {
if (settled) return;
settled = true;
clearTimeout(timeout);
observer.disconnect();
if (added) markAddedInDOM(guidKey, src);
resolve(added);
}
observer.observe(cart, { attributes: true, attributeFilter: ['class'] });
try {
cart.click();
if (cart.classList.contains('icon_cart_clicked')) finish(true);
} catch (error) {
console.log('Add to cart error', error);
finish(false);
}
});
} else {
request = (async () => {
const server = (window.SERVERROOT !== undefined) ? String(window.SERVERROOT) : '/';
const url = server.replace(/\/?$/, '/') + 'cart?add=' + encodeURIComponent(guidKey);
try {
const resp = await fetch(url, { method: 'POST', credentials: 'same-origin' });
if (resp.ok) {
markAddedInDOM(guidKey, src);
return true;
} else {
console.log('Add to cart failed', await resp.text());
}
} catch (err) {
console.log('Add to cart error', err);
}
return false;
})();
}
request = request.finally(() => EBDS_PENDING_CART_REQUESTS.delete(guidKey));
EBDS_PENDING_CART_REQUESTS.set(guidKey, request);
return request;
}
function markAddedInDOM(guidToUse, src) {
// When an item is marked added/hidden, also record its GUID for this session
try { if (guidToUse) addClickedGuid(guidToUse); } catch (e) { }
const orig = guidToUse ? document.querySelector('img.ebds-preview-img[data-guid="' + guidToUse + '"]') : null;
if (orig) {
const wrapper = orig.closest('.ebds-preview');
try { if (wrapper) wrapper.remove(); else orig.remove(); } catch (e) { }
}
// Remove gallery items matching by data-guid or src
try {
const overlay = document.getElementById('ebds-gallery-overlay');
if (overlay) {
const galleryItems = Array.from(overlay.querySelectorAll('.ebds-gallery-item')).filter(item => {
const img = item.querySelector('img');
return (guidToUse && item.dataset.guid === guidToUse)
|| (img && guidToUse && img.dataset.guid === guidToUse)
|| (img && src && img.src === src);
});
galleryItems.forEach(item => {
try { item.remove(); } catch (e) { }
});
try {
if (typeof window.ebdsUpdateGalleryFooter === 'function') window.ebdsUpdateGalleryFooter();
} catch (e) { }
}
} catch (e) { }
}
document.addEventListener('keydown', (e) => {
if (e.key === 'Control') {
ctrlPressed = true;
}
});
document.addEventListener('keyup', (e) => {
if (e.key === 'Control') {
ctrlPressed = false;
}
});
function applyRowFiltering() {
if (!EBDS_ENABLED) return;
const rows = document.querySelectorAll('tr');
rows.forEach(row => {
const hasOriginalImage = row.dataset && row.dataset.ebdsHasImgLink === '1';
const isOperationsRow = (row.id === 'nzb_multi_operations') || !!row.querySelector('#nzb_multi_operations');
const isReleaseRow = (!isOperationsRow) && (
(row.id && row.id.indexOf('guid') === 0) ||
!!row.querySelector('.icon_cart') ||
!!row.querySelector('input[type="checkbox"][name^="nzbs"]')
);
const titleText = isReleaseRow ? getReleaseTitleText(row) : '';
const containsBlacklistedTerm = EBDS_BLACKLIST_TERMS.some(term => term && titleText.includes(term));
const prefixHidden = matchesHiddenPrefixForRow(row);
const missingOriginalImage = EBDS_HIDE_WITHOUT_PICTURES && isReleaseRow && !hasOriginalImage;
const fileSizeMb = isReleaseRow ? parseFileSizeMb(row.textContent) : null;
const belowMinimumSize = EBDS_MIN_FILE_SIZE_MB > 0 && fileSizeMb !== null && fileSizeMb < EBDS_MIN_FILE_SIZE_MB;
const bypassedByAnyFilter =
(EBDS_TEMPORARY_FILTER_BYPASS.keywords && containsBlacklistedTerm) ||
(EBDS_TEMPORARY_FILTER_BYPASS.withoutPictures && missingOriginalImage) ||
(EBDS_TEMPORARY_FILTER_BYPASS.minimumSize && belowMinimumSize);
const shouldHide = prefixHidden ||
(containsBlacklistedTerm && !EBDS_TEMPORARY_FILTER_BYPASS.keywords) ||
(missingOriginalImage && !EBDS_TEMPORARY_FILTER_BYPASS.withoutPictures) ||
(belowMinimumSize && !EBDS_TEMPORARY_FILTER_BYPASS.minimumSize);
row.classList.toggle('ebds-temporarily-shown', bypassedByAnyFilter && !shouldHide);
if (shouldHide) {
row.dataset.ebdsHidden = '1';
row.style.display = 'none';
// Also remove from gallery if present
const guid = row.id && row.id.indexOf('guid') === 0 ? row.id.substring(4) : null;
if (guid) {
const overlay = document.getElementById('ebds-gallery-overlay');
if (overlay && overlay.classList.contains('visible')) {
const galleryImgs = Array.from(overlay.querySelectorAll('img')).filter(i => i.dataset.guid == guid);
galleryImgs.forEach(imgElem => {
const itemWrapper = imgElem.closest('.ebds-gallery-item');
if (itemWrapper) itemWrapper.remove();
});
}
}
} else if (row.dataset.ebdsHidden === '1') {
row.style.display = '';
delete row.dataset.ebdsHidden;
}
});
try {
if (typeof window.ebdsUpdateGalleryFooter === 'function') window.ebdsUpdateGalleryFooter();
} catch (e) { }
try {
if (typeof window.ebdsRefreshGallery === 'function') window.ebdsRefreshGallery();
} catch (e) { }
}
(function () {
'use strict';
if (!EBDS_ENABLED) { console.log('ebds: disabled - t not in 6000-6999'); return; }
function isSupportedImageUrl(value) {
try {
return /\.(?:jpe?g|gif|png)$/i.test(new URL(value, location.href).pathname);
} catch (e) {
return false;
}
}
function createImagePreview(link) {
let div = document.createElement('div');
div.classList.add('ebds-preview');
div.style.display = 'inline-block';
div.style.margin = '10px';
div.style.position = 'relative';
let img = document.createElement('img');
img.classList.add('ebds-preview-img');
// Use thumbnail src if available, else the link href
let thumbSrc = link.querySelector('img') ? link.querySelector('img').src : link.href;
img.src = thumbSrc;
img.style.maxWidth = '400px';
img.style.maxHeight = '400px';
img.style.display = 'block';
img.style.cursor = 'pointer';
// Capture row/guid/cart at creation time so click handler can always find them
let containingRow = link.closest('tr');
let capturedGuid = null;
if (containingRow && containingRow.id && containingRow.id.indexOf('guid') === 0) {
capturedGuid = containingRow.id.substring(4);
}
console.log('Preview created:', link.href, 'row?', !!containingRow, 'guid', capturedGuid);
// expose a few data attributes so gallery items can add to cart later
if (capturedGuid) try { img.dataset.guid = capturedGuid; } catch (e) { }
try { img.dataset.href = link.href; } catch (e) { }
if (containingRow && containingRow.id) try { img.dataset.rowId = containingRow.id; } catch (e) { };
// Shift+Left-click on inline preview => hide this prefix for the session
img.addEventListener('click', function (e) {
try {
if (e.shiftKey && e.button === 0) {
e.preventDefault();
e.stopImmediatePropagation();
const p = derivePrefixFromElement(img);
if (p) addHiddenPrefix(p);
}
} catch (ex) { }
});
// Try to derive a display name and file size from the row (best-effort)
let displayName = null;
let displaySize = null;
let displayPosted = null;
let detailsHref = null;
try {
if (containingRow) {
// Prefer a non-image link's text for the release name
const nameAnchor = Array.from(containingRow.querySelectorAll('a')).find(a => a.href !== link.href && !isSupportedImageUrl(a.href) && a.textContent.trim().length > 0);
if (nameAnchor) displayName = nameAnchor.textContent.trim();
// Fallback: try the third cell which commonly holds the name
if (!displayName) {
const nameCell = containingRow.querySelector('td:nth-child(3)');
if (nameCell) displayName = nameCell.textContent.trim();
}
// Size: look for patterns like "700 MB", "1.2 GB", etc.
const sizeMatch = containingRow.textContent.match(/\b\d+(?:[.,]\d+)?\s*(KB|MB|GB|TB)\b/i);
if (sizeMatch) displaySize = sizeMatch[0].trim();
// Posted time: look for td with class "less mid"
const timeCell = containingRow.querySelector('td.less.mid');
if (timeCell) displayPosted = timeCell.textContent.trim();
// Details href: look for a.title
const titleLink = containingRow.querySelector('a.title');
if (titleLink) detailsHref = titleLink.href;
}
} catch (e) { }
if (displayName) try { img.dataset.name = displayName; } catch (e) { }
if (displaySize) try { img.dataset.size = displaySize; } catch (e) { }
if (displayPosted) try { img.dataset.posted = displayPosted; } catch (e) { }
if (detailsHref) try { img.dataset.detailsHref = detailsHref; } catch (e) { }
// Store caption data for the gallery (do not display inline in the listing)
try {
const posted = displayPosted || '';
const name = displayName || '';
const size = displaySize || '';
let fullText;
if (size) {
const prefix = posted ? posted + ' • ' : '';
const suffix = ' • ' + size;
fullText = prefix + name + suffix;
} else {
const parts = [posted, name].filter(Boolean);
fullText = parts.join(' • ');
}
if (fullText.trim()) img.title = fullText.trim(); // tooltip only
} catch (e) { }
// Create enlarged image container (full-screen modal like before)
let enlargedContainer = document.createElement('div');
enlargedContainer.style.position = 'fixed';
// Make sure this is on top of the gallery overlay and floating buttons
enlargedContainer.style.zIndex = '2147483660';
enlargedContainer.style.display = 'none';
enlargedContainer.style.top = '0';
enlargedContainer.style.left = '0';
enlargedContainer.style.width = '100vw';
enlargedContainer.style.height = '100vh';
enlargedContainer.style.backgroundColor = 'rgba(0,0,0,0.8)';
enlargedContainer.style.alignItems = 'center';
enlargedContainer.style.justifyContent = 'center';
let enlargedImg = document.createElement('img');
enlargedImg.style.maxWidth = '90vw';
enlargedImg.style.maxHeight = '90vh';
enlargedImg.style.objectFit = 'contain';
enlargedContainer.appendChild(enlargedImg);
// mark for global close handling and allow clicking the overlay to close the enlarged preview
enlargedContainer.classList.add('ebds-enlarged');
enlargedContainer.addEventListener('click', function (ev) {
if (ev.target === enlargedContainer) {
enlargedContainer.style.display = 'none';
}
});
// Append to body so it's not constrained by parent stacking contexts and will appear above overlays
document.body.appendChild(enlargedContainer);
// Clicking the thumbnail opens the enlarged preview (click enlarged image to add to cart)
img.addEventListener('click', function (e) {
e.preventDefault();
if (!enlargedImg.getAttribute('src')) enlargedImg.src = link.href;
enlargedContainer.style.display = 'flex';
enlargedContainer.dataset.openedAt = String(Date.now());
});
// Add Ctrl+hover for lightbox preview
img.addEventListener('mouseenter', (e) => {
if (e.ctrlKey || ctrlPressed) openHoverLightbox(img, img.src);
});
img.addEventListener('mousemove', (e) => {
if (e.ctrlKey || ctrlPressed) openHoverLightbox(img, img.src);
else closeHoverLightbox(img);
});
img.addEventListener('mouseleave', () => {
if (!ctrlPressed) closeHoverLightbox(img);
});
// Clicking the enlarged image should add the item to cart (same as cart icon)
enlargedImg.addEventListener('click', function (e) {
e.preventDefault();
// If the enlarged preview was just opened via thumbnail click, ignore the first immediate click to avoid accidental add
try {
const openedAt = enlargedContainer.dataset.openedAt ? parseInt(enlargedContainer.dataset.openedAt, 10) : 0;
if (openedAt && (Date.now() - openedAt) < 300) {
delete enlargedContainer.dataset.openedAt;
return;
}
} catch (ex) { }
// Hide/disable the original thumbnail immediately to avoid layout/mouse events causing flicker
try { img.style.visibility = 'hidden'; img.style.pointerEvents = 'none'; } catch (ex) { }
// Remove the thumbnail wrapper from the listing and update gallery if present
function restoreOnError(delay) {
setTimeout(() => {
try { img.style.visibility = 'visible'; img.style.pointerEvents = 'auto'; } catch (ex) { }
}, delay);
}
console.log('Preview clicked for', link.href);
// Determine the GUID using captured values or live DOM as fallback.
let effectiveRow = containingRow || link.closest('tr') || null;
if (!effectiveRow) console.log('Preview: no row found via captured or live lookup');
let guid = capturedGuid || (effectiveRow && effectiveRow.id && effectiveRow.id.indexOf('guid') === 0 ? effectiveRow.id.substring(4) : null);
if (!guid) {
console.log('Preview: cannot determine cart or guid for this preview');
restoreOnError(400);
return;
}
addToCartByGuid(guid, link.href).then(added => {
if (added) enlargedContainer.remove();
else restoreOnError(400);
});
});
// Insert thumbnail inline (previous behavior)
div.appendChild(img);
link.parentNode.insertBefore(div, link.nextSibling);
}
window.hoverLightboxes = new Map();
// Single shared listener to close all enlarged previews on scroll/resize
function closeAllEnlarged() {
document.querySelectorAll('.ebds-enlarged').forEach(el => {
if (el.style.display !== 'none') el.style.display = 'none';
});
}
['scroll', 'wheel', 'touchmove'].forEach(evt => {
window.addEventListener(evt, closeAllEnlarged, { passive: true });
});
window.addEventListener('resize', closeAllEnlarged);
function openHoverLightbox(img, src) {
if (window.hoverLightboxes.has(img)) return;
const lb = document.createElement('div');
lb.className = 'ebds-hover-lb';
lb.style.position = 'fixed';
lb.style.inset = '0';
lb.style.background = 'rgba(0,0,0,0.95)';
lb.style.display = 'flex';
lb.style.alignItems = 'center';
lb.style.justifyContent = 'center';
lb.style.zIndex = '2147483650';
const im = document.createElement('img');
im.src = src;
applyCtrlHoverPreviewSizing(im);
lb.appendChild(im);
im.style.cursor = 'pointer';
im.title = 'Click to add to cart';
im.addEventListener('click', (ev) => {
ev.stopPropagation();
const guid = img.dataset.guid;
if (guid) {
addToCartByGuid(guid, img.src);
lb.remove();
window.hoverLightboxes.delete(img);
}
});
lb.addEventListener('click', () => {
lb.remove();
window.hoverLightboxes.delete(img);
});
document.body.appendChild(lb);
window.hoverLightboxes.set(img, lb);
}
function closeHoverLightbox(img) {
const lb = window.hoverLightboxes.get(img);
if (lb) {
lb.remove();
window.hoverLightboxes.delete(img);
}
}
function processRows(rows) {
Array.from(rows).forEach(row => {
if (row.dataset.ebdsPreviewProcessed === '1') return;
const imageLinks = Array.from(row.querySelectorAll('a[href]')).filter(link => isSupportedImageUrl(link.href));
if (imageLinks.length > 0) {
try { row.dataset.ebdsHasImgLink = '1'; } catch (e) { }
const preferredLink = imageLinks.find(link => link.querySelector('img')) || imageLinks[0];
createImagePreview(preferredLink);
}
row.dataset.ebdsPreviewProcessed = '1';
Array.from(row.querySelectorAll('a')).forEach(link => {
const text = link.textContent.trim();
if (text === 'Thumbnail' || text === 'Preview') link.remove();
});
});
}
window.ebdsProcessRows = processRows;
processRows(document.querySelectorAll('tr'));
})();
(function () {
'use strict';
if (!EBDS_ENABLED) return;
// Inject CSS for gallery button and overlay
const style = document.createElement('style');
style.textContent = `
.ebds-gallery-button{
position:fixed;
right:16px;
bottom:16px;
transform:none;
z-index:2147483646;
background:#0d6efd;
color:white;
border:none;
border-radius:50%;
width:56px;
height:56px;
display:flex;
align-items:center;
justify-content:center;
box-shadow:0 6px 18px rgba(0,0,0,0.25);
cursor:pointer;
font-weight:600;
font-size:14px;
}
.ebds-gallery-button.active{
background:white;
color:#0d6efd;
}
.ebds-config-button{
z-index:2147483646;
background:#f0ad4e;
color:#1b1f24;
border:none;
border-radius:10px;
padding:8px 12px;
min-width:72px;
display:inline-flex;
align-items:center;
justify-content:center;
box-shadow:0 6px 18px rgba(0,0,0,0.18);
cursor:pointer;
font-weight:700;
font-size:12px;
line-height:1.2;
gap:6px;
}
.ebds-config-button.ebds-floating{
position:fixed;
top:12px;
right:16px;
transform:none;
}
.ebds-sab-button{
position:fixed;
top:12px;
right:96px;
z-index:2147483646;
background:#6c757d;
color:white;
border:none;
border-radius:10px;
padding:7px 9px;
min-width:82px;
cursor:pointer;
font-weight:700;
font-size:11px;
line-height:1.2;
box-shadow:0 6px 18px rgba(0,0,0,0.18);
}
.ebds-sab-button:disabled{ cursor:wait; opacity:0.8; }
.ebds-sab-button.ebds-success{ background:#198754; }
.ebds-sab-button.ebds-error{ background:#dc3545; }
.ebds-config-panel{
position:fixed;
right:16px;
top:60px;
bottom:auto;
width:320px;
max-width:90vw;
background:#0f0f0f;
color:#eee;
border:1px solid rgba(255,255,255,0.08);
border-radius:12px;
padding:12px;
box-shadow:0 12px 32px rgba(0,0,0,0.55);
z-index:2147483647;
display:none;
max-height:80vh;
overflow-y:auto;
}
.ebds-config-panel.visible{ display:block; }
.ebds-config-title{ font-weight:700; font-size:14px; margin-bottom:4px; }
.ebds-config-desc{ color:#b5b5b5; font-size:12px; margin-bottom:8px; }
.ebds-config-row{ display:flex; gap:8px; }
.ebds-config-input{ flex:1; padding:7px 8px; border-radius:8px; border:1px solid #2b2b2b; background:#1a1a1a; color:#eee; }
.ebds-config-add{ background:#0d6efd; color:white; border:none; border-radius:8px; padding:7px 10px; font-weight:700; cursor:pointer; }
.ebds-chip-list{ display:flex; flex-wrap:wrap; gap:8px; margin-top:10px; }
.ebds-chip{ background:#191919; border:1px solid #2d2d2d; padding:6px 8px; border-radius:999px; display:inline-flex; align-items:center; gap:6px; font-size:12px; }
.ebds-chip button{ background:none; border:none; color:#bbb; cursor:pointer; font-weight:700; }
.ebds-chip button:hover{ color:#fff; }
.ebds-config-empty{ color:#888; font-size:12px; }
.ebds-config-actions{ display:flex; justify-content:flex-end; margin-top:10px; }
.ebds-config-close{ background:#343a40; color:white; border:none; border-radius:8px; padding:6px 10px; cursor:pointer; }
.ebds-config-toggle{ display:flex; align-items:center; gap:8px; margin:0; font-size:12px; }
.ebds-config-toggle input[type="checkbox"]{ width:16px; height:16px; }
.ebds-config-toggle input[type="range"]{ -webkit-appearance: none; appearance: none; background: #ddd; height: 6px; border-radius: 3px; flex:1; }
.ebds-config-toggle input[type="range"]::-webkit-slider-thumb{ -webkit-appearance: none; appearance: none; width: 16px; height: 16px; background: #0d6efd; border-radius: 50%; cursor: pointer; }
.ebds-config-toggle input[type="range"]::-moz-range-thumb{ width: 16px; height: 16px; background: #0d6efd; border-radius: 50%; cursor: pointer; border: none; }
.ebds-category-toggles{
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
#ebds-gallery-overlay{
position:fixed;
inset:0;
z-index:2147483645;
background:rgba(0,0,0,0.9);
display:none;
padding:24px;
overflow:auto;
}
#ebds-gallery-overlay.visible{ display:block; }
.ebds-gallery-grid{
display:grid;
grid-template-columns: repeat(var(--ebds-cols, 3), minmax(0, 1fr));
gap:16px;
align-items:start;
}
.ebds-gallery-item img{
width:100%;
height:auto;
max-height:500px;
display:block;
border-radius:6px;
box-shadow:0 4px 12px rgba(0,0,0,0.5);
cursor:pointer;
}
.ebds-gallery-placeholder-visual{
min-height:180px;
display:flex;
align-items:center;
justify-content:center;
border:1px dashed #555;
border-radius:6px;
background:#191919;
color:#888;
font-size:14px;
text-decoration:none;
cursor:pointer;
}
.ebds-gallery-placeholder-visual:hover{ color:#bbb; border-color:#777; }
.ebds-gallery-item.ebds-temporarily-shown img{
box-shadow:0 0 0 2px #ff8c00, 0 4px 12px rgba(0,0,0,0.5);
}
.ebds-gallery-item.ebds-temporarily-shown .ebds-preview-caption{ color:#ff8c00; }
.ebds-gallery-item.ebds-temporarily-shown .ebds-gallery-placeholder-visual{
color:#ff8c00;
border-color:#ff8c00;
}
/* When the vertical limiter is disabled, allow tall images to expand */
#ebds-gallery-overlay.ebds-gallery-no-vertical-limit .ebds-gallery-item img{
max-height:none;
}
.ebds-gallery-item{
position:relative;
min-width:0;
overflow:hidden;
}
.ebds-preview-caption{
display:block;
width:100%;
margin-top:6px;
font-size:12px;
color:#ddd;
max-width:100%;
overflow:hidden;
text-overflow:ellipsis;
white-space:nowrap;
}
/* Highlight for posts newer than last visit */
.ebds-preview-caption.ebds-new-post{
color: #7ab800; /* green */
}
.ebds-gallery-category-links{
margin-top:0px;
display:flex;
justify-content:center;
gap:10px;
flex-wrap:wrap;
}
.ebds-gallery-category-links a{
color:#ddd;
text-decoration:none;
}
.ebds-gallery-category-links a:hover{
color:#fff;
}
.ebds-gallery-pagination{
margin-top:0px;
display:flex;
justify-content:center;
color:#ddd;
}
.ebds-gallery-pagination-spacer.ebds-top{ padding:4px 0 16px; }
.ebds-gallery-pagination-spacer.ebds-bottom{ padding:16px 0 4px; }
.ebds-gallery-pagination .pagination{ margin:0; }
.ebds-gallery-pagination a, .ebds-gallery-pagination span { color: #ddd; }
body.ebds-infinite-active nav.pagination,
body.ebds-infinite-active nav[aria-label="Pagination"],
body.ebds-infinite-active ul.pagination{ display:none !important; }
.ebds-infinite-status{
display:block;
width:100%;
padding:16px;
border:0;
background:transparent;
color:#aaa;
font-size:12px;
text-align:center;
cursor:default;
}
.ebds-infinite-status[hidden]{ display:none; }
.ebds-infinite-status.ebds-retry{ color:#f0a040; cursor:pointer; }
.ebds-gallery-filter-links{
padding:16px 0 4px;
color:#aaa;
font-size:12px;
text-align:center;
}
.ebds-gallery-filter-links.ebds-header{ padding:4px 0 16px; }
.ebds-gallery-filter-links a{ color:#f0a040; text-decoration:none; }
.ebds-gallery-filter-links a:hover{ color:#ffc166; text-decoration:underline; }
.ebds-gallery-filter-links a.ebds-filter-bypassed{
color:#ff8c00;
font-weight:600;
text-decoration:underline;
}
`;
document.head.appendChild(style);
document.body.classList.toggle('ebds-infinite-active', EBDS_INFINITE_SCROLL);
let EBDS_GALLERY_COLS = 3;
try {
const storedCols = parseInt(localStorage.getItem('ebdsGalleryCols'), 10);
if (Number.isFinite(storedCols)) EBDS_GALLERY_COLS = Math.min(10, Math.max(1, storedCols));
} catch (e) { }
// Gallery vertical limiter: when true, images are constrained to max-height (default: disabled)
let EBDS_GALLERY_VERTICAL_LIMIT = false;
try {
const v = localStorage.getItem('ebdsGalleryVerticalLimit');
if (v !== null) EBDS_GALLERY_VERTICAL_LIMIT = (v === '1');
} catch (e) { }
// Option to only show new items in gallery (default false)
let EBDS_GALLERY_SHOW_ONLY_NEW = false;
try { EBDS_GALLERY_SHOW_ONLY_NEW = (localStorage.getItem('ebdsGalleryShowOnlyNew') === '1'); } catch (e) { }
let EBDS_SESSION_EXPIRATION_MINUTES = 5;
try {
const storedMinutes = parseInt(localStorage.getItem(EBDS_SESSION_EXPIRATION_KEY), 10);
if (Number.isFinite(storedMinutes)) EBDS_SESSION_EXPIRATION_MINUTES = Math.min(1440, Math.max(1, storedMinutes));
} catch (e) { }
const categoryLinks = [
{ id: 'todaysTop', href: '/browse?t=6000&top=1', title: 'Todays Top Grabs', text: 'Todays Top Grabs' },
{ id: 'allXxx', href: '/browse?t=6000', text: 'All XXX' },
{ id: 'dvd', href: '/browse?t=6010', text: 'DVD' },
{ id: 'hd', href: '/browse?t=6040', text: 'HD' },
{ id: 'other', href: '/browse?t=6999', text: 'Other' },
{ id: 'packs', href: '/browse?t=6070', text: 'Packs' },
{ id: 'sd', href: '/browse?t=6080', text: 'SD' },
{ id: 'uhd', href: '/browse?t=6045', text: 'UHD' },
{ id: 'vr', href: '/browse?t=6050', text: 'VR' },
{ id: 'wmv', href: '/browse?t=6020', text: 'WMV' },
{ id: 'xvid', href: '/browse?t=6030', text: 'XviD' }
];
// Create floating button
const btn = document.createElement('button');
btn.className = 'ebds-gallery-button';
btn.title = 'Open image gallery (G)';
btn.textContent = 'Gallery';
document.body.appendChild(btn);
// Configuration button and panel for blacklist keywords
const configBtn = document.createElement('button');
configBtn.className = 'ebds-config-button ebds-floating';
configBtn.title = 'Configure EBDS';
configBtn.textContent = 'Config';
document.body.appendChild(configBtn);
const sabBtn = document.createElement('button');
sabBtn.className = 'ebds-sab-button';
sabBtn.hidden = true;
sabBtn.title = 'Tell SABnzbd to read and process all RSS feeds now';
sabBtn.textContent = 'Read feeds';
document.body.appendChild(sabBtn);
const configPanel = document.createElement('div');
configPanel.className = 'ebds-config-panel';
configPanel.innerHTML = `
<div class="ebds-config-title">Keyboard shortcuts</div>
<div class="ebds-config-desc">G: Gallery | ←/→: Navigate | Esc: Close | Ctrl-hover: Preview</div>
<div class="ebds-config-toggle">
<input class="ebds-infinite-scroll-toggle" type="checkbox" id="ebdsInfiniteScrollToggle">
<label for="ebdsInfiniteScrollToggle">Load the next page while scrolling (infinite scrolling Beta)</label>
</div>
<div class="ebds-config-toggle">
<input class="ebds-gallery-only-new-toggle" type="checkbox" id="ebdsGalleryOnlyNewToggle">
<label for="ebdsGalleryOnlyNewToggle">Show only new items in gallery</label>
</div>
<div class="ebds-config-toggle">
<input class="ebds-hide-nopics-toggle" type="checkbox" id="ebdsHideNoPicsToggle">
<label for="ebdsHideNoPicsToggle">Hide items without pictures</label>
</div>
<div class="ebds-config-toggle">
<label for="ebdsMinFileSizeInput">Minimum filesize (MB, 0 disables)</label>
<input id="ebdsMinFileSizeInput" type="number" min="0" step="0.01" value="0" style="width:90px; padding:4px;">
<button id="ebdsMinFileSizeSet" type="button" class="ebds-config-add" style="padding:6px 8px;">Set</button>
</div>
<div class="ebds-config-toggle">
<input class="ebds-vertical-limit-toggle" type="checkbox" id="ebdsVerticalLimitToggle">
<label for="ebdsVerticalLimitToggle">Limit gallery image height</label>
</div>
<div class="ebds-config-toggle">
<input class="ebds-scale-hover-toggle" type="checkbox" id="ebdsScaleHoverToggle">
<label for="ebdsScaleHoverToggle">Scale Ctrl-hover preview to browser size</label>
</div>
<div class="ebds-config-toggle">
<label for="ebdsFilterLinksPosition">Filter count links</label>
<select id="ebdsFilterLinksPosition">
<option value="footer">Footer</option>
<option value="header">Header</option>
<option value="hidden">Not shown</option>
</select>
</div>
<div class="ebds-config-toggle">
<label for="ebdsGalleryCols">Gallery columns: <span id="ebdsGalleryColsValue">3</span></label>
<input type="range" id="ebdsGalleryCols" min="1" max="10" value="3">
</div>
<div class="ebds-config-toggle">
<label for="ebdsSessionExpirationInput">Session expiration (mins): <span id="ebdsSessionExpirationValue">5</span></label>
<input type="number" id="ebdsSessionExpirationInput" min="1" max="1440" value="5" style="width:80px; padding:4px;">
<button id="ebdsSessionExpirationSet" type="button" class="ebds-config-add" style="margin-left:8px; padding:6px 8px;">Set</button>
</div>
<div class="ebds-config-title" style="margin-top:10px;">SABnzbd</div>
<div class="ebds-config-desc">Credentials are stored by the userscript manager.</div>
<div class="ebds-config-toggle" style="margin-bottom:8px;">
<input class="ebds-hide-sab-button-toggle" type="checkbox" id="ebdsHideSabButtonToggle">
<label for="ebdsHideSabButtonToggle">Hide Read feeds button</label>
</div>
<div class="ebds-config-row" style="margin-bottom:8px;">
<input id="ebdsSabUrl" class="ebds-config-input" type="url" inputmode="url" autocomplete="off" placeholder="https://sabnzbd.example.com" aria-label="SABnzbd URL">
</div>
<div class="ebds-config-row">
<input id="ebdsSabApiKey" class="ebds-config-input" type="password" autocomplete="new-password" data-bwignore="true" placeholder="SABnzbd API key" aria-label="SABnzbd API key">
<button id="ebdsSabSave" class="ebds-config-add" type="button">Save</button>
</div>
<div id="ebdsSabStatus" class="ebds-config-desc" style="margin-top:6px; margin-bottom:0;"></div>
<div class="ebds-config-title">Exclude keywords</div>
<div class="ebds-config-desc">Release titles containing these terms will be hidden.</div>
<div class="ebds-config-row">
<input class="ebds-config-input ebds-blacklist-input" type="text" placeholder="Add keyword..." aria-label="Add keyword to exclude">
<button class="ebds-config-add ebds-blacklist-add" type="button">Add</button>
</div>
<div class="ebds-chip-list"></div>
<div class="ebds-config-actions"><button class="ebds-config-close" type="button">Close</button></div>
`;
document.body.appendChild(configPanel);
const categoryLinksDiv = document.createElement('div');
categoryLinksDiv.innerHTML = '<div class="ebds-config-title">Category links visibility</div><div class="ebds-config-desc">Choose which category links to show in the gallery.</div><div class="ebds-category-toggles"></div>';
configPanel.appendChild(categoryLinksDiv);
const actions = configPanel.querySelector('.ebds-config-actions');
if (actions) {
configPanel.insertBefore(categoryLinksDiv, actions);
}
const EBDS_LINK_VISIBILITY_KEY = 'ebdsCategoryLinksVisibility';
function loadLinkVisibility() {
try {
const stored = JSON.parse(localStorage.getItem(EBDS_LINK_VISIBILITY_KEY) || '{}');
return stored;
} catch (e) {
return {};
}
}
function persistLinkVisibility(vis) {
try {
localStorage.setItem(EBDS_LINK_VISIBILITY_KEY, JSON.stringify(vis));
} catch (e) {}
}
let EBDS_LINK_VISIBILITY = loadLinkVisibility();
categoryLinks.forEach(link => {
const toggleDiv = document.createElement('div');
toggleDiv.className = 'ebds-config-toggle';
const input = document.createElement('input');
input.type = 'checkbox';
input.id = 'ebdsLink' + link.id;
input.checked = EBDS_LINK_VISIBILITY[link.id] !== false;
input.addEventListener('change', () => {
EBDS_LINK_VISIBILITY[link.id] = input.checked;
persistLinkVisibility(EBDS_LINK_VISIBILITY);
});
const label = document.createElement('label');
label.htmlFor = input.id;
label.textContent = link.text;
toggleDiv.appendChild(input);
toggleDiv.appendChild(label);
categoryLinksDiv.querySelector('.ebds-category-toggles').appendChild(toggleDiv);
});
const blacklistInput = configPanel.querySelector('.ebds-blacklist-input');
const blacklistList = configPanel.querySelector('.ebds-chip-list');
const addBlacklistBtn = configPanel.querySelector('.ebds-blacklist-add');
const closeConfigBtn = configPanel.querySelector('.ebds-config-close');
const hidePicsToggle = configPanel.querySelector('.ebds-hide-nopics-toggle');
const minFileSizeInput = configPanel.querySelector('#ebdsMinFileSizeInput');
const minFileSizeSetBtn = configPanel.querySelector('#ebdsMinFileSizeSet');
const verticalLimitToggle = configPanel.querySelector('.ebds-vertical-limit-toggle');
const scaleHoverToggle = configPanel.querySelector('.ebds-scale-hover-toggle');
const filterLinksPositionSelect = configPanel.querySelector('#ebdsFilterLinksPosition');
const galleryOnlyNewToggle = configPanel.querySelector('.ebds-gallery-only-new-toggle');
const infiniteScrollToggle = configPanel.querySelector('.ebds-infinite-scroll-toggle');
const sabUrlInput = configPanel.querySelector('#ebdsSabUrl');
const sabApiKeyInput = configPanel.querySelector('#ebdsSabApiKey');
const sabSaveBtn = configPanel.querySelector('#ebdsSabSave');
const sabStatus = configPanel.querySelector('#ebdsSabStatus');
const hideSabButtonToggle = configPanel.querySelector('.ebds-hide-sab-button-toggle');
function loadSabConfig() {
try {
return {
url: String(GM_getValue(EBDS_SAB_URL_KEY, '') || ''),
apiKey: String(GM_getValue(EBDS_SAB_API_KEY, '') || '')
};
} catch (e) {
return { url: '', apiKey: '' };
}
}
function normalizeSabUrl(rawUrl) {
const url = new URL(String(rawUrl || '').trim());
if (url.protocol !== 'http:' && url.protocol !== 'https:') throw new Error('URL must use HTTP or HTTPS');
url.username = '';
url.password = '';
url.search = '';
url.hash = '';
url.pathname = url.pathname.replace(/\/+$/, '');
return url.toString().replace(/\/$/, '');
}
function setSabStatus(message, isError = false) {
if (!sabStatus) return;
sabStatus.textContent = message;
sabStatus.style.color = isError ? '#ff7b7b' : '#7ab800';
}
function updateSabButtonVisibility(config = loadSabConfig()) {
sabBtn.hidden = EBDS_HIDE_SAB_BUTTON || !(config.url && config.apiKey);
}
function buildSabApiUrl(config, params) {
const apiUrl = new URL(config.url);
apiUrl.pathname = apiUrl.pathname.replace(/\/+$/, '');
if (!apiUrl.pathname.endsWith('/api')) apiUrl.pathname += '/api';
apiUrl.search = '';
apiUrl.searchParams.set('output', 'json');
apiUrl.searchParams.set('apikey', config.apiKey);
Object.entries(params).forEach(([key, value]) => apiUrl.searchParams.set(key, String(value)));
return apiUrl;
}
function requestSab(config, params) {
return new Promise((resolve, reject) => {
let apiUrl;
try {
apiUrl = buildSabApiUrl(config, params);
} catch (e) {
reject(new Error('Invalid SABnzbd URL'));
return;
}
GM_xmlhttpRequest({
method: 'GET',
url: apiUrl.toString(),
timeout: 15000,
responseType: 'json',
onload: response => {
let data = response.response;
if (!data && response.responseText) {
try { data = JSON.parse(response.responseText); } catch (e) { }
}
if (response.status < 200 || response.status >= 300) {
reject(new Error(data && data.error ? data.error : `HTTP ${response.status || 'error'}`));
return;
}
if (data && data.error) {
reject(new Error(data.error));
return;
}
resolve(data);
},
ontimeout: () => reject(new Error('SABnzbd request timed out')),
onerror: () => reject(new Error('Unable to connect to SABnzbd'))
});
});
}
async function saveSabConfig() {
sabSaveBtn.disabled = true;
sabSaveBtn.textContent = 'Testing…';
try {
const url = normalizeSabUrl(sabUrlInput.value);
const existingConfig = loadSabConfig();
const apiKey = sabApiKeyInput.value.trim() || existingConfig.apiKey;
if (!apiKey) throw new Error('API key is required');
const config = { url, apiKey };
const response = await requestSab(config, { mode: 'queue', start: 0, limit: 1 });
if (!response || !response.queue) throw new Error('Unexpected response from SABnzbd');
await GM_setValue(EBDS_SAB_URL_KEY, url);
await GM_setValue(EBDS_SAB_API_KEY, apiKey);
sabUrlInput.value = url;
sabApiKeyInput.value = '';
sabApiKeyInput.placeholder = 'API key saved';
updateSabButtonVisibility(config);
setSabStatus('Connection verified and saved.');
return true;
} catch (e) {
setSabStatus(e.message || 'Unable to verify SABnzbd settings.', true);
return false;
} finally {
sabSaveBtn.disabled = false;
sabSaveBtn.textContent = 'Save';
}
}
function setSabButtonState(label, state = '') {
sabBtn.textContent = label;
sabBtn.classList.remove('ebds-success', 'ebds-error');
if (state) sabBtn.classList.add(state);
}
async function triggerSabFeeds() {
const config = loadSabConfig();
if (!config.url || !config.apiKey) {
updateSabButtonVisibility(config);
return;
}
sabBtn.disabled = true;
setSabButtonState('Reading…');
try {
const response = await requestSab(config, { mode: 'rss_now' });
if (!response || response.status !== true) throw new Error('SABnzbd did not accept the request');
setSabButtonState('Feeds requested', 'ebds-success');
setTimeout(() => setSabButtonState('Read feeds'), 2500);
} catch (e) {
setSabButtonState('SAB error', 'ebds-error');
setSabStatus(e.message || 'SABnzbd request failed.', true);
setTimeout(() => setSabButtonState('Read feeds'), 3000);
} finally {
sabBtn.disabled = false;
}
}
const initialSabConfig = loadSabConfig();
sabUrlInput.value = initialSabConfig.url;
if (initialSabConfig.apiKey) sabApiKeyInput.placeholder = 'API key saved';
updateSabButtonVisibility(initialSabConfig);
sabSaveBtn.addEventListener('click', saveSabConfig);
sabApiKeyInput.addEventListener('keydown', e => {
if (e.key === 'Enter') saveSabConfig();
});
sabBtn.addEventListener('click', triggerSabFeeds);
if (hideSabButtonToggle) {
hideSabButtonToggle.checked = EBDS_HIDE_SAB_BUTTON;
hideSabButtonToggle.addEventListener('change', () => {
EBDS_HIDE_SAB_BUTTON = !!hideSabButtonToggle.checked;
try { localStorage.setItem(EBDS_HIDE_SAB_BUTTON_KEY, EBDS_HIDE_SAB_BUTTON ? '1' : '0'); } catch (e) { }
updateSabButtonVisibility();
});
}
// Apply vertical limiter state to overlay
function applyGalleryVerticalLimit() {
try {
if (!overlay) return;
if (EBDS_GALLERY_VERTICAL_LIMIT) overlay.classList.remove('ebds-gallery-no-vertical-limit');
else overlay.classList.add('ebds-gallery-no-vertical-limit');
} catch (e) { }
}
// initialize toggle states and bind events
if (hidePicsToggle) {
hidePicsToggle.checked = EBDS_HIDE_WITHOUT_PICTURES;
hidePicsToggle.addEventListener('change', () => {
EBDS_HIDE_WITHOUT_PICTURES = !!hidePicsToggle.checked;
EBDS_TEMPORARY_FILTER_BYPASS.withoutPictures = false;
try { localStorage.setItem(EBDS_HIDE_WITHOUT_PICS_STORAGE_KEY, EBDS_HIDE_WITHOUT_PICTURES ? '1' : '0'); } catch (e) { }
applyRowFiltering();
});
}
if (minFileSizeInput && minFileSizeSetBtn) {
minFileSizeInput.value = String(EBDS_MIN_FILE_SIZE_MB);
function setMinimumFileSize() {
const value = Number(minFileSizeInput.value);
if (!Number.isFinite(value) || value < 0) {
minFileSizeInput.setCustomValidity('Enter a value of 0 or greater.');
minFileSizeInput.reportValidity();
return;
}
minFileSizeInput.setCustomValidity('');
EBDS_MIN_FILE_SIZE_MB = value;
EBDS_TEMPORARY_FILTER_BYPASS.minimumSize = false;
minFileSizeInput.value = String(value);
try { localStorage.setItem(EBDS_MIN_FILE_SIZE_MB_KEY, String(value)); } catch (e) { }
applyRowFiltering();
}
minFileSizeSetBtn.addEventListener('click', setMinimumFileSize);
minFileSizeInput.addEventListener('keydown', e => {
if (e.key === 'Enter') setMinimumFileSize();
});
}
if (verticalLimitToggle) {
verticalLimitToggle.checked = EBDS_GALLERY_VERTICAL_LIMIT;
verticalLimitToggle.addEventListener('change', () => {
EBDS_GALLERY_VERTICAL_LIMIT = !!verticalLimitToggle.checked;
try { localStorage.setItem('ebdsGalleryVerticalLimit', EBDS_GALLERY_VERTICAL_LIMIT ? '1' : '0'); } catch (e) { }
applyGalleryVerticalLimit();
});
}
if (scaleHoverToggle) {
scaleHoverToggle.checked = EBDS_SCALE_HOVER_PREVIEW;
scaleHoverToggle.addEventListener('change', () => {
EBDS_SCALE_HOVER_PREVIEW = !!scaleHoverToggle.checked;
try { localStorage.setItem(EBDS_SCALE_HOVER_PREVIEW_KEY, EBDS_SCALE_HOVER_PREVIEW ? '1' : '0'); } catch (e) { }
});
}
if (filterLinksPositionSelect) {
filterLinksPositionSelect.value = EBDS_FILTER_LINKS_POSITION;
filterLinksPositionSelect.addEventListener('change', () => {
EBDS_FILTER_LINKS_POSITION = filterLinksPositionSelect.value;
try { localStorage.setItem(EBDS_FILTER_LINKS_POSITION_KEY, EBDS_FILTER_LINKS_POSITION); } catch (e) { }
updateGalleryFooter();
});
}
if (galleryOnlyNewToggle) {
galleryOnlyNewToggle.checked = EBDS_GALLERY_SHOW_ONLY_NEW;
const galleryOnlyNewContainer = galleryOnlyNewToggle.closest('.ebds-config-toggle');
if (galleryOnlyNewContainer && EBDS_IS_SEARCH_PAGE) galleryOnlyNewContainer.style.display = 'none';
galleryOnlyNewToggle.addEventListener('change', () => {
EBDS_GALLERY_SHOW_ONLY_NEW = !!galleryOnlyNewToggle.checked;
try { localStorage.setItem('ebdsGalleryShowOnlyNew', EBDS_GALLERY_SHOW_ONLY_NEW ? '1' : '0'); } catch (e) { }
try { if (overlay && overlay.classList && overlay.classList.contains('visible')) openGallery(); } catch (e) { }
try {
if (typeof window.ebdsRefreshInfiniteScroll === 'function') window.ebdsRefreshInfiniteScroll();
} catch (e) { }
});
}
if (infiniteScrollToggle) {
infiniteScrollToggle.checked = EBDS_INFINITE_SCROLL;
infiniteScrollToggle.addEventListener('change', () => {
EBDS_INFINITE_SCROLL = !!infiniteScrollToggle.checked;
try { localStorage.setItem(EBDS_INFINITE_SCROLL_KEY, EBDS_INFINITE_SCROLL ? '1' : '0'); } catch (e) { }
document.body.classList.toggle('ebds-infinite-active', EBDS_INFINITE_SCROLL);
try {
if (typeof window.ebdsSetInfiniteScrollEnabled === 'function') {
window.ebdsSetInfiniteScrollEnabled(EBDS_INFINITE_SCROLL);
}
} catch (e) { }
try { if (overlay.classList.contains('visible')) openGallery(); } catch (e) { }
});
}
const galleryColsSlider = configPanel.querySelector('#ebdsGalleryCols');
const galleryColsValue = configPanel.querySelector('#ebdsGalleryColsValue');
if (galleryColsSlider && galleryColsValue) {
galleryColsSlider.value = EBDS_GALLERY_COLS;
galleryColsValue.textContent = EBDS_GALLERY_COLS;
galleryColsSlider.addEventListener('input', () => {
const val = parseInt(galleryColsSlider.value);
EBDS_GALLERY_COLS = val;
galleryColsValue.textContent = val;
try { localStorage.setItem('ebdsGalleryCols', val.toString()); } catch (e) { }
// Update grid columns
grid.style.setProperty('--ebds-cols', val);
});
}
// Session expiration minutes control
const sessionExpirationInput = configPanel.querySelector('#ebdsSessionExpirationInput');
const sessionExpirationValue = configPanel.querySelector('#ebdsSessionExpirationValue');
const sessionExpirationSetBtn = configPanel.querySelector('#ebdsSessionExpirationSet');
if (sessionExpirationInput && sessionExpirationValue) {
sessionExpirationInput.value = EBDS_SESSION_EXPIRATION_MINUTES;
sessionExpirationValue.textContent = EBDS_SESSION_EXPIRATION_MINUTES;
function applySessionExpiration() {
let m = parseInt(sessionExpirationInput.value, 10);
if (!Number.isFinite(m) || m < 1) m = 1;
if (m > 1440) m = 1440;
EBDS_SESSION_EXPIRATION_MINUTES = m;
sessionExpirationInput.value = m;
sessionExpirationValue.textContent = m;
try { localStorage.setItem(EBDS_SESSION_EXPIRATION_KEY, String(m)); } catch (e) { }
}
sessionExpirationSetBtn && sessionExpirationSetBtn.addEventListener('click', applySessionExpiration);
sessionExpirationInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') applySessionExpiration(); });
sessionExpirationInput.addEventListener('blur', applySessionExpiration);
}
function renderBlacklistChips() {
blacklistList.innerHTML = '';
if (!EBDS_BLACKLIST_TERMS.length) {
const empty = document.createElement('div');
empty.className = 'ebds-config-empty';
empty.textContent = 'No keywords yet.';
blacklistList.appendChild(empty);
return;
}
EBDS_BLACKLIST_TERMS.forEach(term => {
const chip = document.createElement('div');
chip.className = 'ebds-chip';
const label = document.createElement('span');
label.textContent = term;
const removeBtn = document.createElement('button');
removeBtn.type = 'button';
removeBtn.title = 'Remove keyword';
removeBtn.textContent = 'x';
removeBtn.addEventListener('click', () => {
EBDS_BLACKLIST_TERMS = EBDS_BLACKLIST_TERMS.filter(t => t !== term);
EBDS_TEMPORARY_FILTER_BYPASS.keywords = false;
persistBlacklistTerms(EBDS_BLACKLIST_TERMS);
applyRowFiltering();
renderBlacklistChips();
});
chip.appendChild(label);
chip.appendChild(removeBtn);
blacklistList.appendChild(chip);
});
}
function addBlacklistTerm(raw) {
const normalized = normalizeBlacklistTerm(raw);
if (!normalized) return;
if (!EBDS_BLACKLIST_TERMS.includes(normalized)) {
EBDS_BLACKLIST_TERMS.push(normalized);
EBDS_TEMPORARY_FILTER_BYPASS.keywords = false;
persistBlacklistTerms(EBDS_BLACKLIST_TERMS);
applyRowFiltering();
renderBlacklistChips();
}
}
addBlacklistBtn.addEventListener('click', () => {
addBlacklistTerm(blacklistInput.value);
blacklistInput.value = '';
blacklistInput.focus();
});
blacklistInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
addBlacklistTerm(blacklistInput.value);
blacklistInput.value = '';
blacklistInput.focus();
}
});
configBtn.addEventListener('click', () => {
const nowVisible = !configPanel.classList.contains('visible');
configPanel.classList.toggle('visible');
if (nowVisible) {
renderBlacklistChips();
if (hidePicsToggle) hidePicsToggle.checked = EBDS_HIDE_WITHOUT_PICTURES;
if (verticalLimitToggle) verticalLimitToggle.checked = EBDS_GALLERY_VERTICAL_LIMIT;
if (scaleHoverToggle) scaleHoverToggle.checked = EBDS_SCALE_HOVER_PREVIEW;
if (filterLinksPositionSelect) filterLinksPositionSelect.value = EBDS_FILTER_LINKS_POSITION;
if (galleryOnlyNewToggle) galleryOnlyNewToggle.checked = EBDS_GALLERY_SHOW_ONLY_NEW;
if (infiniteScrollToggle) infiniteScrollToggle.checked = EBDS_INFINITE_SCROLL;
if (hideSabButtonToggle) hideSabButtonToggle.checked = EBDS_HIDE_SAB_BUTTON;
try { blacklistInput.focus(); } catch (e) { }
}
});
closeConfigBtn.addEventListener('click', () => {
configPanel.classList.remove('visible');
});
// Keep Config button floating on the right side of the viewport
// Create overlay
const overlay = document.createElement('div');
overlay.id = 'ebds-gallery-overlay';
const grid = document.createElement('div');
grid.className = 'ebds-gallery-grid';
overlay.appendChild(grid);
document.body.appendChild(overlay);
// Ensure the vertical limiter state is applied now.
try { applyGalleryVerticalLimit(); } catch (e) { }
// Quick state for gallery hover lightboxes
let galleryHoverLightboxes = new Map();
let currentHoveredGalleryItem = null;
// Track whether Control is currently pressed so we don't accidentally close the hover lightbox while it's held
// using global ctrlPressed
// preserve previous display value of the main browse container so we can hide/show it
let browseContainerPrevDisplay = null;
// Set initial columns
grid.style.setProperty('--ebds-cols', EBDS_GALLERY_COLS);
// --- EBDS: per-category session and saved state helpers ---
const EBDS_SESSION_PREFIX = 'ebdSession_';
const EBDS_SAVED_PREFIX = 'ebdSaved_';
const EBDS_SAVED_EXPIRED_PREFIX = 'ebdSavedExpired_';
const EBDS_BASELINE_PENDING_PREFIX = 'ebdBaselinePending_';
function getCategoryIdFromUrl() {
try {
const params = new URLSearchParams(location.search);
const t = params.get('t');
if (t) {
const top = params.get('top');
const match = categoryLinks.find(link => {
const linkUrl = new URL(link.href, location.origin);
if (linkUrl.searchParams.get('t') !== t) return false;
return top === '1' ? linkUrl.searchParams.get('top') === '1' : !linkUrl.searchParams.has('top');
});
if (match) return match.id;
}
// Fallback: try to match a visible category link by pathname/search
const href = location.pathname + (location.search || '');
const match = categoryLinks.find(l => href.indexOf(l.href) !== -1 || l.href.indexOf(href) !== -1);
if (match) return match.id;
} catch (e) { }
return null;
}
function parseRowTitleEpoch(row) {
try {
if (!row) return null;
// Look for the first time cell with a title attribute: <td class="less mid" title="YYYY-MM-DD HH:MM:SS">...
const timeCell = row.querySelector('td.less.mid[title]');
if (!timeCell) return null;
const t = timeCell.getAttribute('title');
if (!t) return null;
// Convert to ISO-ish form to be parsed as local datetime: replace first space with 'T'
const iso = t.replace(' ', 'T');
const d = new Date(iso);
const ms = d.getTime();
return Number.isFinite(ms) ? ms : null;
} catch (e) { return null; }
}
function getSessionTimestamp(catId) {
try {
if (!catId) return 0;
const v = localStorage.getItem(EBDS_SESSION_PREFIX + catId);
if (!v) return 0;
const n = parseInt(v, 10);
return Number.isFinite(n) ? n : 0;
} catch (e) { return 0; }
}
function setSessionTimestamp(catId, epochMs) {
try {
if (!catId || !epochMs) return;
localStorage.setItem(EBDS_SESSION_PREFIX + catId, String(Number(epochMs)));
} catch (e) { }
}
function getSavedTimestamp(catId) {
try {
if (!catId) return 0;
const v = localStorage.getItem(EBDS_SAVED_PREFIX + catId);
if (!v) return 0;
const n = parseInt(v, 10);
return Number.isFinite(n) ? n : 0;
} catch (e) { return 0; }
}
function setSavedTimestamp(catId, epochMs) {
try {
if (!catId || !epochMs) return;
localStorage.setItem(EBDS_SAVED_PREFIX + catId, String(Number(epochMs)));
} catch (e) { }
}
function getSavedExpiredTimestamp(catId) {
try {
if (!catId) return 0;
const v = localStorage.getItem(EBDS_SAVED_EXPIRED_PREFIX + catId);
if (!v) return 0;
const n = parseInt(v, 10);
return Number.isFinite(n) ? n : 0;
} catch (e) { return 0; }
}
function setSavedExpiredTimestamp(catId, epochMs) {
try {
if (!catId || !epochMs) return;
localStorage.setItem(EBDS_SAVED_EXPIRED_PREFIX + catId, String(Number(epochMs)));
} catch (e) { }
}
function getLastVisitTimestamp(catId) {
// For highlighting, use ebdSavedExpired
return getSavedExpiredTimestamp(catId);
}
function setBaselinePending(catId, pending) {
try {
const key = EBDS_BASELINE_PENDING_PREFIX + catId;
if (pending) localStorage.setItem(key, '1');
else localStorage.removeItem(key);
} catch (e) { }
}
function isBaselinePending(catId) {
try { return localStorage.getItem(EBDS_BASELINE_PENDING_PREFIX + catId) === '1'; }
catch (e) { return false; }
}
function saveFirstPageBaseline(catId) {
const firstRow = document.querySelector('tr[id^="guid"]');
const epoch = firstRow ? parseRowTitleEpoch(firstRow) : null;
if (!epoch) return false;
setSavedTimestamp(catId, epoch);
setBaselinePending(catId, false);
return true;
}
function updateUiSessionOnPageLoad() {
try {
const now = Date.now();
const expirationMs = EBDS_SESSION_EXPIRATION_MINUTES * 60 * 1000;
const previous = parseInt(localStorage.getItem(EBDS_UI_SESSION_KEY), 10);
if (!Number.isFinite(previous) || (now - previous) >= expirationMs) {
clearClickedGuids();
clearHiddenPrefixes();
}
localStorage.setItem(EBDS_UI_SESSION_KEY, String(now));
} catch (e) { }
}
function updateLastVisitFromPageOnLoad() {
try {
if (EBDS_IS_SEARCH_PAGE) return;
const catId = getCategoryIdFromUrl();
if (!catId) return;
const params = new URLSearchParams(location.search);
const offset = params.get('offset');
const now = Date.now();
const sessionExpirationMs = EBDS_SESSION_EXPIRATION_MINUTES * 60 * 1000;
const lastSession = getSessionTimestamp(catId);
const sessionExpired = !lastSession || (now - lastSession) >= sessionExpirationMs;
if (sessionExpired) {
// Session expired: backup ebdSaved to ebdSavedExpired if exists
const currentSaved = getSavedTimestamp(catId);
if (currentSaved) {
setSavedExpiredTimestamp(catId, currentSaved);
}
// Update session timestamp
setSessionTimestamp(catId, now);
// If on first page, save the first item's timestamp to ebdSaved
if (!offset || offset === '0') {
if (!saveFirstPageBaseline(catId)) setBaselinePending(catId, true);
} else {
setBaselinePending(catId, true);
}
} else {
// Session not expired: just update session timestamp
setSessionTimestamp(catId, now);
if ((!offset || offset === '0') && isBaselinePending(catId)) {
saveFirstPageBaseline(catId);
}
}
} catch (e) { }
}
function updateSessionActivityForInfiniteScroll() {
updateUiSessionOnPageLoad();
if (EBDS_IS_SEARCH_PAGE) return;
const catId = getCategoryIdFromUrl();
if (!catId) return;
const now = Date.now();
const lastSession = getSessionTimestamp(catId);
const expirationMs = EBDS_SESSION_EXPIRATION_MINUTES * 60 * 1000;
if (!lastSession || (now - lastSession) >= expirationMs) {
const currentSaved = getSavedTimestamp(catId);
if (currentSaved) setSavedExpiredTimestamp(catId, currentSaved);
// Loaded pages may be older, so defer the new baseline until page one is visited.
setBaselinePending(catId, true);
}
setSessionTimestamp(catId, now);
}
function getGalleryOmissionCounts() {
const counts = { withoutPictures: 0, excludedByKeywords: 0, belowMinimumSize: 0 };
const currentCategoryId = getCategoryIdFromUrl();
const lastVisit = currentCategoryId ? getLastVisitTimestamp(currentCategoryId) : 0;
const showOnlyNew = EBDS_GALLERY_SHOW_ONLY_NEW && !EBDS_IS_SEARCH_PAGE && lastVisit > 0;
document.querySelectorAll('tr[id^="guid"]').forEach(row => {
try {
if (showOnlyNew) {
const itemEpoch = parseRowTitleEpoch(row);
if (!(itemEpoch && itemEpoch > lastVisit)) return;
}
const titleText = getReleaseTitleText(row);
const blacklisted = EBDS_BLACKLIST_TERMS.some(term => term && titleText.includes(term));
const hasOriginalImage = row.dataset.ebdsHasImgLink === '1';
const missingOriginalImage = EBDS_HIDE_WITHOUT_PICTURES && !hasOriginalImage;
const fileSizeMb = parseFileSizeMb(row.textContent);
const belowMinimumSize = EBDS_MIN_FILE_SIZE_MB > 0 && fileSizeMb !== null && fileSizeMb < EBDS_MIN_FILE_SIZE_MB;
if (missingOriginalImage) counts.withoutPictures++;
if (blacklisted) counts.excludedByKeywords++;
if (belowMinimumSize) counts.belowMinimumSize++;
} catch (e) { }
});
return counts;
}
function updateGalleryFooter() {
try {
const existingLinks = overlay.querySelector('.ebds-gallery-filter-links');
if (existingLinks) existingLinks.remove();
if (!overlay.classList.contains('visible') || EBDS_FILTER_LINKS_POSITION === 'hidden') return;
const counts = getGalleryOmissionCounts();
const filters = [
{
count: counts.withoutPictures,
key: 'withoutPictures',
label: `${counts.withoutPictures} ${counts.withoutPictures === 1 ? 'item' : 'items'} without pictures`
},
{
count: counts.excludedByKeywords,
key: 'keywords',
label: `${counts.excludedByKeywords} excluded by keywords`
},
{
count: counts.belowMinimumSize,
key: 'minimumSize',
label: `${counts.belowMinimumSize} below minimum size`
}
].filter(filter => filter.count > 0);
if (!filters.length) return;
const linksContainer = document.createElement('div');
linksContainer.className = 'ebds-gallery-filter-links';
if (EBDS_FILTER_LINKS_POSITION === 'header') linksContainer.classList.add('ebds-header');
filters.forEach((filter, index) => {
if (index > 0) linksContainer.appendChild(document.createTextNode(' • '));
const link = document.createElement('a');
link.href = '#';
const bypassed = EBDS_TEMPORARY_FILTER_BYPASS[filter.key];
link.textContent = filter.label;
link.title = bypassed ? 'Apply this filter again' : 'Show these items temporarily';
link.classList.toggle('ebds-filter-bypassed', bypassed);
link.addEventListener('click', event => {
event.preventDefault();
EBDS_TEMPORARY_FILTER_BYPASS[filter.key] = !EBDS_TEMPORARY_FILTER_BYPASS[filter.key];
applyRowFiltering();
});
linksContainer.appendChild(link);
});
if (EBDS_FILTER_LINKS_POSITION === 'header') {
const topPagination = overlay.querySelector('.ebds-gallery-pagination.ebds-top');
if (topPagination) topPagination.insertAdjacentElement('afterend', linksContainer);
else overlay.insertBefore(linksContainer, grid);
} else {
overlay.appendChild(linksContainer);
}
} catch (e) { }
}
window.ebdsUpdateGalleryFooter = updateGalleryFooter;
function formatGalleryCaption(posted, name, size) {
const maxLength = 80;
if (!size) {
const text = [posted, name].filter(Boolean).join(' • ');
return text.length > maxLength ? text.substring(0, maxLength - 1) + '…' : text;
}
const prefix = posted ? posted + ' • ' : '';
const suffix = ' • ' + size;
const fullText = prefix + name + suffix;
if (fullText.length <= maxLength) return fullText;
const availableNameLength = maxLength - prefix.length - suffix.length;
const truncatedName = availableNameLength > 1
? name.substring(0, availableNameLength - 1) + '…'
: '';
return prefix + truncatedName + suffix;
}
function openGallery() {
grid.innerHTML = '';
// determine last-visit epoch for the current category (if any)
const currentCategoryId = getCategoryIdFromUrl();
const lastVisit = currentCategoryId ? getLastVisitTimestamp(currentCategoryId) : 0;
const showOnlyNew = EBDS_GALLERY_SHOW_ONLY_NEW && !EBDS_IS_SEARCH_PAGE && lastVisit > 0;
const imgs = Array.from(document.querySelectorAll('.ebds-preview-img'));
imgs.forEach((img, idx) => {
// Skip images from hidden rows (filtered out)
const rowId = img.dataset.rowId;
const sourceRow = rowId ? document.getElementById(rowId) : null;
if (sourceRow && sourceRow.style.display === 'none') return;
// Skip images that were clicked/added during this session
try {
const g = img.dataset.guid || img.getAttribute('data-guid');
if (g && EBDS_CLICKED_GUIDS && EBDS_CLICKED_GUIDS.has(g)) return;
} catch (e) { }
try {
const p = derivePrefixFromElement(img) || derivePrefixFromSrc(img.src);
if (p && EBDS_HIDDEN_PREFIXES && EBDS_HIDDEN_PREFIXES.has(p)) return;
} catch (e) { }
const item = document.createElement('div');
item.className = 'ebds-gallery-item';
if (rowId) item.dataset.rowId = rowId;
if (sourceRow && sourceRow.classList.contains('ebds-temporarily-shown')) {
item.classList.add('ebds-temporarily-shown');
}
const copy = img.cloneNode(true);
// Remove inline transition handlers to avoid duplication issues
copy.style.maxWidth = '';
copy.style.maxHeight = '';
copy.addEventListener('click', (e) => {
// Shift+Left-click => hide by prefix for this session
try {
if (e.shiftKey && e.button === 0) {
e.preventDefault();
e.stopPropagation();
const p = derivePrefixFromElement(copy);
if (p) addHiddenPrefix(p);
return;
}
} catch (ex) { }
e.preventDefault();
const guid = copy.dataset.guid || (copy.getAttribute('data-guid') || null);
if (guid) addToCartForGuid(guid, copy.src);
else addToCartBySrc(copy.src);
});
item.appendChild(copy);
// add caption under clone (use stored data attributes if available)
try {
const detailsHref = copy.dataset.detailsHref;
const caption = document.createElement(detailsHref ? 'a' : 'div');
caption.className = 'ebds-preview-caption';
if (detailsHref) {
caption.href = detailsHref;
caption.style.textDecoration = 'none';
}
const posted = copy.dataset.posted || '';
const name = copy.dataset.name || '';
const size = copy.dataset.size || '';
caption.textContent = formatGalleryCaption(posted, name, size);
try {
// Mark as new when the parsed row epoch is newer than stored last visit for this category
const row = copy.dataset.rowId ? document.getElementById(copy.dataset.rowId) : null;
const itemEpoch = row ? parseRowTitleEpoch(row) : null;
if (itemEpoch && lastVisit && itemEpoch > lastVisit) {
caption.classList.add('ebds-new-post');
}
} catch (e) { }
// Optionally hide items that aren't new
try {
const row = copy.dataset.rowId ? document.getElementById(copy.dataset.rowId) : null;
const itemEpoch = row ? parseRowTitleEpoch(row) : null;
const isNew = (itemEpoch && lastVisit && itemEpoch > lastVisit);
if (showOnlyNew && !isNew) {
// Skip adding this item to the gallery
return;
}
} catch (e) { }
item.appendChild(caption);
} catch (e) { }
// Add ctrl+hover behavior: open a large preview while holding Control
try {
copy.addEventListener('mouseenter', (e) => {
currentHoveredGalleryItem = item;
if (e.ctrlKey || ctrlPressed) openGalleryHoverLightbox(item, copy.src);
});
copy.addEventListener('mousemove', (e) => {
// open when ctrl is indicated (either via event or global flag); don't close while ctrl is held
if (e.ctrlKey || ctrlPressed) openGalleryHoverLightbox(item, copy.src);
else closeGalleryHoverLightbox(item);
});
copy.addEventListener('mouseleave', (e) => {
currentHoveredGalleryItem = null;
// Only close when Ctrl is NOT pressed
if (!ctrlPressed) closeGalleryHoverLightbox(item);
});
} catch (ex) { }
grid.appendChild(item);
});
if (!EBDS_HIDE_WITHOUT_PICTURES || EBDS_TEMPORARY_FILTER_BYPASS.withoutPictures) {
document.querySelectorAll('tr[id^="guid"]').forEach(row => {
try {
if (row.style.display === 'none' || row.dataset.ebdsHasImgLink === '1') return;
const guid = row.id.substring(4);
if (guid && EBDS_CLICKED_GUIDS.has(guid)) return;
const itemEpoch = parseRowTitleEpoch(row);
if (showOnlyNew && !(itemEpoch && itemEpoch > lastVisit)) return;
const item = document.createElement('div');
item.className = 'ebds-gallery-item ebds-gallery-placeholder';
if (EBDS_TEMPORARY_FILTER_BYPASS.withoutPictures) {
item.classList.add('ebds-temporarily-shown');
}
item.dataset.guid = guid;
item.dataset.rowId = row.id;
const detailsLink = row.querySelector('a.title');
const visual = document.createElement('div');
visual.className = 'ebds-gallery-placeholder-visual';
visual.textContent = 'No picture';
visual.title = 'Click to add to cart';
visual.addEventListener('click', event => {
event.preventDefault();
addToCartForGuid(guid, null);
});
item.appendChild(visual);
const caption = document.createElement(detailsLink ? 'a' : 'div');
caption.className = 'ebds-preview-caption';
if (detailsLink) {
caption.href = detailsLink.href;
caption.style.textDecoration = 'none';
}
const name = detailsLink ? detailsLink.textContent.trim() : '';
const sizeMatch = row.textContent.match(/\b\d+(?:[.,]\d+)?\s*(?:KB|MB|GB|TB)\b/i);
const postedCell = row.querySelector('td.less.mid');
caption.textContent = formatGalleryCaption(
postedCell ? postedCell.textContent.trim() : '',
name,
sizeMatch ? sizeMatch[0].trim() : ''
);
if (itemEpoch && lastVisit && itemEpoch > lastVisit) {
caption.classList.add('ebds-new-post');
}
item.appendChild(caption);
grid.appendChild(item);
} catch (e) { }
});
}
// Image cards and no-picture placeholders are built separately; restore source-row order.
const rowOrder = new Map(
Array.from(document.querySelectorAll('#browsetable tr[id^="guid"]'))
.map((row, index) => [row.id, index])
);
Array.from(grid.querySelectorAll('.ebds-gallery-item'))
.sort((left, right) => {
const leftIndex = rowOrder.get(left.dataset.rowId) ?? Number.MAX_SAFE_INTEGER;
const rightIndex = rowOrder.get(right.dataset.rowId) ?? Number.MAX_SAFE_INTEGER;
return leftIndex - rightIndex;
})
.forEach(item => grid.appendChild(item));
// Add category links at the top of gallery
try {
// remove existing category links if present
const oldLinks = overlay.querySelectorAll('.ebds-gallery-category-links');
oldLinks.forEach(l => l.remove());
const linksDiv = document.createElement('div');
linksDiv.className = 'ebds-gallery-category-links';
const links = categoryLinks;
const visibleLinks = links.filter(link => EBDS_LINK_VISIBILITY[link.id] !== false);
for (let i = 0; i < visibleLinks.length; i++) {
const link = visibleLinks[i];
const a = document.createElement('a');
a.href = link.href;
if (link.title) a.title = link.title;
a.textContent = link.text;
a.addEventListener('click', () => { try { localStorage.setItem('ebdsGalleryOpen', '1'); } catch (e) { } });
linksDiv.appendChild(a);
if (i < visibleLinks.length - 1) {
linksDiv.appendChild(document.createTextNode(' | '));
}
}
overlay.insertBefore(linksDiv, grid);
} catch (e) { }
// Append page pagination to top and bottom of gallery overlay (if present on the page)
try {
// remove existing gallery pagination clones if present
const oldPags = overlay.querySelectorAll('.ebds-gallery-pagination, .ebds-gallery-pagination-spacer');
oldPags.forEach(p => p.remove());
const pageNav = document.querySelector('nav.pagination[aria-label="Pagination"]');
if (EBDS_INFINITE_SCROLL) {
const topSpacer = document.createElement('div');
topSpacer.className = 'ebds-gallery-pagination-spacer ebds-top';
topSpacer.setAttribute('aria-hidden', 'true');
const bottomSpacer = document.createElement('div');
bottomSpacer.className = 'ebds-gallery-pagination-spacer ebds-bottom';
bottomSpacer.setAttribute('aria-hidden', 'true');
overlay.insertBefore(topSpacer, grid);
grid.insertAdjacentElement('afterend', bottomSpacer);
} else if (pageNav) {
const clonedNavTop = pageNav.cloneNode(true);
clonedNavTop.classList.add('ebds-gallery-pagination', 'ebds-top');
const clonedNavBottom = pageNav.cloneNode(true);
clonedNavBottom.classList.add('ebds-gallery-pagination', 'ebds-bottom');
// Keep gallery open across pages: do NOT close the gallery when pagination links are clicked.
// Instead ensure the persistent flag is set so the new page will re-open the gallery.
[clonedNavTop, clonedNavBottom].forEach(clonedNav => {
try { clonedNav.querySelectorAll('a').forEach(a => a.addEventListener('click', () => { try { localStorage.setItem('ebdsGalleryOpen', '1'); } catch (e) { } })); } catch (e) { }
});
// If "Show only new items" is enabled, and the current page contains at least one non-new item,
// prune cloned pagination entries after the current page (these pages will contain older/non-new items).
try {
if (showOnlyNew) {
const rowsOnPage = Array.from(document.querySelectorAll('tr[id^="guid"]'));
const hasNonNew = rowsOnPage.some(r => {
try {
const epoch = parseRowTitleEpoch(r);
// treat missing epoch as non-new
if (!epoch) return true;
return lastVisit && epoch <= lastVisit;
} catch (e) { return false; }
});
if (hasNonNew) {
function pruneAfterCurrent(nav) {
try {
const current = nav.querySelector('[aria-current="page"]') || nav.querySelector('.active, li.active, .current') || nav.querySelector('a.active');
const curLi = current && current.closest ? (current.closest('li') || current) : null;
if (curLi) {
let sib = curLi.nextElementSibling;
while (sib) {
const next = sib.nextElementSibling;
try { sib.remove(); } catch (e) { }
sib = next;
}
} else {
// Fallback: remove subsequent anchors after the active anchor
const anchors = Array.from(nav.querySelectorAll('a'));
let idx = anchors.findIndex(a => a === current || a.classList.contains('active') || a.getAttribute('aria-current') === 'page');
if (idx >= 0) {
for (let i = anchors.length - 1; i > idx; i--) {
try { anchors[i].remove(); } catch (e) { }
}
} else {
// As a final fallback, remove obvious "next" links
const labels = /next|›|»|>|right|→/i;
anchors.filter(a => a.getAttribute('rel') === 'next' || labels.test(a.getAttribute('aria-label') || '') || labels.test(a.textContent)).forEach(a => { try { a.remove(); } catch (e) { } });
}
}
} catch (e) { }
}
pruneAfterCurrent(clonedNavTop);
pruneAfterCurrent(clonedNavBottom);
}
}
} catch (e) { }
overlay.insertBefore(clonedNavTop, grid);
overlay.appendChild(clonedNavBottom);
}
} catch (e) { }
try {
const container = document.querySelector('div.container-fluid');
if (container) {
if (!container.hasAttribute('data-ebds-hidden')) {
browseContainerPrevDisplay = container.style.display;
container.style.display = 'none';
container.setAttribute('data-ebds-hidden', '1');
}
}
} catch (e) { }
try { applyGalleryVerticalLimit(); } catch (e) { }
overlay.classList.add('visible');
btn.classList.add('active');
updateGalleryFooter();
try { localStorage.setItem('ebdsGalleryOpen', '1'); } catch (e) { }
}
function closeGallery() {
try {
const container = document.querySelector('div.container-fluid');
if (container && browseContainerPrevDisplay !== null) {
container.style.display = browseContainerPrevDisplay;
container.removeAttribute('data-ebds-hidden');
}
browseContainerPrevDisplay = null;
} catch (e) { }
overlay.classList.remove('visible');
btn.classList.remove('active');
try { localStorage.removeItem('ebdsGalleryOpen'); } catch (e) { }
}
// Hover lightboxes inside the gallery (opened while Ctrl is held)
function openGalleryHoverLightbox(item, src) {
try {
if (galleryHoverLightboxes.has(item)) return;
const lb = document.createElement('div');
lb.className = 'ebds-gallery-hover-lb';
lb.style.position = 'fixed';
lb.style.inset = '0';
lb.style.background = 'rgba(0,0,0,0.95)';
lb.style.display = 'flex';
lb.style.alignItems = 'center';
lb.style.justifyContent = 'center';
lb.style.zIndex = '2147483650';
const im = document.createElement('img');
im.src = src;
applyCtrlHoverPreviewSizing(im);
lb.appendChild(im);
// image click => add to cart; background click closes lightbox. (Escape handled globally)
im.style.cursor = 'pointer';
im.title = 'Click to add to cart';
im.addEventListener('click', (ev) => {
ev.stopPropagation();
try {
const guidImg = item.querySelector('img');
const guid = guidImg && guidImg.dataset ? (guidImg.dataset.guid || null) : null;
if (guid) addToCartForGuid(guid, src);
else addToCartBySrc(src);
} catch (e) { console.log('hover-lightbox add-to-cart error', e); }
try { lb.remove(); } catch (e) { }
try { galleryHoverLightboxes.delete(item); } catch (e) { }
});
lb.addEventListener('click', () => {
try { lb.remove(); } catch (e) { }
galleryHoverLightboxes.delete(item);
});
document.body.appendChild(lb);
galleryHoverLightboxes.set(item, lb);
} catch (e) { console.log('openGalleryHoverLightbox error', e); }
}
function closeGalleryHoverLightbox(item) {
try {
const lb = galleryHoverLightboxes.get(item);
if (lb) {
try { lb.remove(); } catch (e) { }
galleryHoverLightboxes.delete(item);
}
} catch (e) { }
}
// Open hover lightbox when Control is pressed while hovering over a gallery item
document.addEventListener('keydown', (e) => {
if (e.key === 'Control') {
ctrlPressed = true;
try {
if (currentHoveredGalleryItem) {
const img = currentHoveredGalleryItem.querySelector('img');
if (img) openGalleryHoverLightbox(currentHoveredGalleryItem, img.src);
}
} catch (ex) { console.log('keydown ctrl openGalleryHoverLightbox error', ex); }
}
});
// Close hover lightboxes when Ctrl is released
document.addEventListener('keyup', (e) => {
if (e.key === 'Control') {
ctrlPressed = false;
try { galleryHoverLightboxes.forEach((lb, item) => { lb.remove(); }); galleryHoverLightboxes.clear(); } catch (ex) { }
try { window.hoverLightboxes.forEach((lb, img) => { lb.remove(); }); window.hoverLightboxes.clear(); } catch (ex) { }
}
});
// Also close hover lightboxes when gallery closes
const originalCloseGallery = closeGallery;
closeGallery = function () {
try { galleryHoverLightboxes.forEach((lb, item) => { lb.remove(); }); galleryHoverLightboxes.clear(); } catch (ex) { }
originalCloseGallery();
};
// --- helper: add to cart by guid or src ---
function addToCartForGuid(guid, src) {
return addToCartByGuid(guid, src);
}
function addToCartBySrc(src) {
const anchor = Array.from(document.querySelectorAll('a[href]')).find(a => a.href === src);
if (anchor) {
const row = anchor.closest('tr');
const guid = row && row.id && row.id.indexOf('guid') === 0 ? row.id.substring(4) : null;
if (guid) { addToCartForGuid(guid, src); return; }
}
try { if (window.$ && $.pnotify) { $.pnotify({ title: 'CART', text: 'Could not determine item to add', type: 'error' }); } } catch (e) { }
}
window.ebdsRefreshGallery = () => {
if (overlay.classList.contains('visible')) openGallery();
};
btn.addEventListener('click', () => {
if (overlay.classList.contains('visible')) closeGallery();
else openGallery();
});
// Keyboard shortcut helpers for Prev/Next page (ArrowLeft / ArrowRight)
function findPaginationLink(direction) {
try {
// Prefer rel attributes if present
const rel = direction === 'prev' ? 'prev' : 'next';
const relLink = document.querySelector('a[rel="' + rel + '"]');
if (relLink) return relLink;
} catch (e) { }
// Try common pagination containers
const nav = document.querySelector('nav.pagination') || document.querySelector('nav[aria-label="Pagination"]') || document.querySelector('ul.pagination') || document.querySelector('.pagination');
if (!nav) return null;
// Find current page item and walk siblings
const current = nav.querySelector('[aria-current="page"]') || nav.querySelector('.active, li.active, .current') || nav.querySelector('a.active');
if (current) {
const curAnchor = current.tagName === 'A' ? current : (current.querySelector('a') || current);
const li = (curAnchor && curAnchor.closest) ? curAnchor.closest('li') || curAnchor.parentElement : null;
if (li) {
let sib = direction === 'prev' ? li.previousElementSibling : li.nextElementSibling;
while (sib) {
const a = sib.querySelector('a');
if (a && a.getAttribute('href')) return a;
sib = direction === 'prev' ? sib.previousElementSibling : sib.nextElementSibling;
}
}
}
// Fallback: try aria-label or visible text matching Prev/Next
const labels = direction === 'prev' ? /prev|previous|‹|«|<|left|←/i : /next|›|»|>|right|→/i;
const a = Array.from(nav.querySelectorAll('a')).find(a => (a.getAttribute('aria-label') && labels.test(a.getAttribute('aria-label'))) || labels.test(a.textContent));
return a || null;
}
// Keyboard shortcut
document.addEventListener('keydown', (e) => {
// Ignore when typing in form controls or when a focused element exists (covers custom elements/shadow hosts)
try {
const ae = document.activeElement;
// If focus is on anything other than the document body or root, assume the user is interacting with UI and don't intercept
if (ae && ae !== document.body && ae !== document.documentElement) return;
} catch (ex) { }
if (e.key.toLowerCase() === 'g' && !e.altKey && !e.ctrlKey && !e.metaKey) {
e.preventDefault();
if (overlay.classList.contains('visible')) closeGallery();
else openGallery();
} else if (e.key === 'Escape') {
// Close the gallery and any visible enlarged previews
closeGallery();
try { document.querySelectorAll('.ebds-enlarged').forEach(el => { if (el.style.display && el.style.display !== 'none') el.style.display = 'none'; }); } catch (ex) { }
} else if ((e.key === 'ArrowLeft' || e.key === 'ArrowRight') && !e.altKey && !e.ctrlKey && !e.metaKey) {
// Jump to previous/next page using site pagination
const direction = (e.key === 'ArrowLeft') ? 'prev' : 'next';
e.preventDefault();
// Prefer global rel links first
let link = null;
try { link = document.querySelector('a[rel="' + (direction === 'prev' ? 'prev' : 'next') + '"]'); } catch (ex) { }
if (!link) link = findPaginationLink(direction);
// Also check overlay cloned pagination (gallery) for matching links
if (!link && overlay && overlay.querySelector) {
try {
const cloneNav = overlay.querySelector('.ebds-gallery-pagination');
if (cloneNav) {
// look for obvious labels
const labels = direction === 'prev' ? /prev|previous|‹|«|<|left|←/i : /next|›|»|>|right|→/i;
link = Array.from(cloneNav.querySelectorAll('a')).find(a => (a.getAttribute('aria-label') && labels.test(a.getAttribute('aria-label'))) || labels.test(a.textContent));
if (!link) {
const cur = cloneNav.querySelector('[aria-current="page"]') || cloneNav.querySelector('.active, li.active, a.active');
if (cur) {
const li = cur.closest ? cur.closest('li') : null;
if (li) {
let sib = direction === 'prev' ? li.previousElementSibling : li.nextElementSibling;
while (sib) {
const a = sib.querySelector('a');
if (a && a.getAttribute('href')) { link = a; break; }
sib = direction === 'prev' ? sib.previousElementSibling : sib.nextElementSibling;
}
}
}
}
}
} catch (ex) { }
}
if (link) {
try { link.click(); } catch (ex) { try { window.location = link.href; } catch (e) { } }
// Keep gallery open if it was visible so the gallery state persists across navigation
try { if (overlay.classList && overlay.classList.contains('visible')) try { localStorage.setItem('ebdsGalleryOpen', '1'); } catch (e) { } } catch (ex) { }
}
}
});
// Update per-category last-visit on initial page load when on first page (offset missing or =0)
try { updateUiSessionOnPageLoad(); } catch (e) { }
try { updateLastVisitFromPageOnLoad(); } catch (e) { }
try { applyRowFiltering(); } catch (e) { }
// Load subsequent result pages into the current listing and gallery near the scroll boundary.
try {
const resultTable = document.querySelector('#browsetable');
if (resultTable) {
const resultRowsContainer = resultTable.tBodies[0] || resultTable;
const listingStatus = document.createElement('button');
listingStatus.type = 'button';
listingStatus.className = 'ebds-infinite-status';
resultTable.insertAdjacentElement('afterend', listingStatus);
const galleryStatus = document.createElement('button');
galleryStatus.type = 'button';
galleryStatus.className = 'ebds-infinite-status';
overlay.appendChild(galleryStatus);
let loadingNextPage = false;
let nextPageUrl = findNextPageUrl(document, location.href);
let listingObserver = null;
let galleryObserver = null;
let newItemsBoundaryReached = false;
function findNextPageUrl(root, baseUrl) {
const nav = root.querySelector('nav.pagination[aria-label="Pagination"], nav.pagination, ul.pagination, .pagination');
if (!nav) return null;
const current = nav.querySelector('[aria-current="page"], li.active, .current, a.active');
let sibling = current && current.closest ? (current.closest('li') || current).nextElementSibling : null;
while (sibling) {
if (!sibling.classList.contains('disabled')) {
const link = sibling.matches('a[href]') ? sibling : sibling.querySelector('a[href]');
const href = link && link.getAttribute('href');
if (href && href !== '#') {
const resolved = new URL(href, baseUrl);
return resolved.origin === location.origin ? resolved.href : null;
}
}
sibling = sibling.nextElementSibling;
}
return null;
}
function updateInfiniteStatus(message, retryable) {
[listingStatus, galleryStatus].forEach(status => {
status.textContent = message;
status.disabled = !retryable;
status.classList.toggle('ebds-retry', !!retryable);
});
}
function setInfiniteStatusVisible(visible) {
[listingStatus, galleryStatus].forEach(status => { status.hidden = !visible; });
}
function getGalleryPrefetchMargin() {
const items = Array.from(grid.querySelectorAll('.ebds-gallery-item'));
if (items.length === 0) return 1000;
const firstTop = items[0].getBoundingClientRect().top;
const firstRowHeight = items
.filter(item => Math.abs(item.getBoundingClientRect().top - firstTop) < 2)
.reduce((height, item) => Math.max(height, item.getBoundingClientRect().height), 0);
return Math.ceil(600 + firstRowHeight);
}
function isNewItemsOnlyActive() {
if (!EBDS_GALLERY_SHOW_ONLY_NEW || EBDS_IS_SEARCH_PAGE) return false;
const catId = getCategoryIdFromUrl();
return !!(catId && getLastVisitTimestamp(catId));
}
function pageHasNewItems(rows) {
if (!isNewItemsOnlyActive()) return true;
const lastVisit = getLastVisitTimestamp(getCategoryIdFromUrl());
return rows.some(row => {
const epoch = parseRowTitleEpoch(row);
return !!(epoch && epoch > lastVisit);
});
}
async function loadNextPage() {
if (!EBDS_INFINITE_SCROLL || loadingNextPage || !nextPageUrl ||
(isNewItemsOnlyActive() && newItemsBoundaryReached)) return;
loadingNextPage = true;
if (listingObserver) listingObserver.disconnect();
if (galleryObserver) galleryObserver.disconnect();
updateInfiniteStatus('Loading next page…', false);
const requestedUrl = nextPageUrl;
let loadedSuccessfully = false;
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 20000);
try {
const response = await fetch(requestedUrl, {
credentials: 'same-origin',
signal: controller.signal
});
if (!response.ok) throw new Error('HTTP ' + response.status);
const fetchedDocument = new DOMParser().parseFromString(await response.text(), 'text/html');
if (!fetchedDocument.querySelector('#browsetable')) throw new Error('Result table missing from response');
const fetchedRows = Array.from(fetchedDocument.querySelectorAll('#browsetable tr[id^="guid"]'));
const newRows = fetchedRows.filter(row => !document.getElementById(row.id));
if (isNewItemsOnlyActive() && !pageHasNewItems(fetchedRows)) {
newItemsBoundaryReached = true;
}
newRows.forEach(row => {
row.dataset.ebdsInfiniteAdded = '1';
resultRowsContainer.appendChild(row);
});
const followingPageUrl = findNextPageUrl(fetchedDocument, requestedUrl);
nextPageUrl = followingPageUrl && followingPageUrl !== requestedUrl ? followingPageUrl : null;
if (typeof window.ebdsProcessRows === 'function') window.ebdsProcessRows(newRows);
updateSessionActivityForInfiniteScroll();
applyRowFiltering();
loadedSuccessfully = true;
if (!EBDS_INFINITE_SCROLL) updateInfiniteStatus('Infinite scrolling disabled', false);
else if (isNewItemsOnlyActive() && newItemsBoundaryReached) {
setInfiniteStatusVisible(false);
} else if (!nextPageUrl) updateInfiniteStatus('All results loaded', false);
else updateInfiniteStatus('Scroll to load more', false);
} catch (error) {
console.error('EBDS infinite scroll failed:', error);
updateInfiniteStatus(
error && error.name === 'AbortError'
? 'Loading timed out — click to retry'
: 'Could not load more — click to retry',
true
);
} finally {
clearTimeout(timeout);
loadingNextPage = false;
if (loadedSuccessfully) observeInfiniteScroll();
}
}
function observeInfiniteScroll() {
document.body.classList.toggle('ebds-infinite-active', EBDS_INFINITE_SCROLL);
if (listingObserver) listingObserver.disconnect();
if (galleryObserver) galleryObserver.disconnect();
if (!EBDS_INFINITE_SCROLL) {
setInfiniteStatusVisible(false);
updateInfiniteStatus('Infinite scrolling disabled', false);
return;
}
if (isNewItemsOnlyActive() && newItemsBoundaryReached) {
setInfiniteStatusVisible(false);
return;
}
setInfiniteStatusVisible(true);
if (!nextPageUrl) {
updateInfiniteStatus('All results loaded', false);
return;
}
updateInfiniteStatus('Scroll to load more', false);
listingObserver = new IntersectionObserver(entries => {
if (entries.some(entry => entry.isIntersecting)) loadNextPage();
}, { rootMargin: '600px 0px' });
galleryObserver = new IntersectionObserver(entries => {
if (entries.some(entry => entry.isIntersecting)) loadNextPage();
}, { root: overlay, rootMargin: getGalleryPrefetchMargin() + 'px 0px' });
listingObserver.observe(listingStatus);
galleryObserver.observe(galleryStatus);
}
listingStatus.addEventListener('click', loadNextPage);
galleryStatus.addEventListener('click', loadNextPage);
window.ebdsSetInfiniteScrollEnabled = enabled => {
EBDS_INFINITE_SCROLL = !!enabled;
observeInfiniteScroll();
};
window.ebdsRefreshInfiniteScroll = observeInfiniteScroll;
if (isNewItemsOnlyActive()) {
newItemsBoundaryReached = !pageHasNewItems(
Array.from(resultTable.querySelectorAll('tr[id^="guid"]'))
);
}
setInfiniteStatusVisible(false);
const startObserving = () => requestAnimationFrame(observeInfiniteScroll);
if (typeof requestIdleCallback === 'function') requestIdleCallback(startObserving, { timeout: 1000 });
else setTimeout(startObserving, 250);
}
} catch (e) {
console.error('EBDS infinite scroll initialization failed:', e);
}
// Restore state across page loads, including search-result pagination.
try { if (localStorage.getItem('ebdsGalleryOpen') === '1') openGallery(); } catch (e) { }
})();