diff --git a/frontend/js/utils.js b/frontend/js/utils.js
index da4d7cd..fcbc626 100644
--- a/frontend/js/utils.js
+++ b/frontend/js/utils.js
@@ -73,8 +73,8 @@ export function isPlatformAdmin(user) {
// Lazy-load authenticated images. A plain
can't send the Bearer token,
// and thumbnail/file endpoints require auth — a just-uploaded item's thumbnail
-// 403's without it. We fetch with the token and swap in an object URL.
-// IntersectionObserver keeps it lazy; the object URL is revoked after load.
+// 403's without it. We fetch with the token and swap in an object URL, revoked
+// after load.
let _authImgObserver = null;
export function loadAuthImage(img) {
const url = img.dataset.authSrc;
@@ -89,13 +89,17 @@ export function loadAuthImage(img) {
})
.catch(() => { img.style.opacity = '0.25'; });
}
-export function hydrateAuthImages(root) {
+// Hydrate
under `root`. Lazy by default: an
+// IntersectionObserver loads each thumbnail only as it scrolls into view, so a
+// large grid (content library, etc.) doesn't fire a fetch per thumbnail on
+// render. Pass { eager: true } for small, transient surfaces (pickers/modals)
+// where every item is on screen and immediate load reads better.
+export function hydrateAuthImages(root, { eager = false } = {}) {
const imgs = root.querySelectorAll('img[data-auth-src]');
if (!imgs.length) return;
- // Load all images immediately; IntersectionObserver is used below
- // only for images that are off-screen (lazy loading).
- if (typeof IntersectionObserver === 'undefined') {
+ // Eager path (opt-in) and the no-IntersectionObserver fallback both load now.
+ if (eager || typeof IntersectionObserver === 'undefined') {
imgs.forEach(loadAuthImage);
return;
}
@@ -105,8 +109,5 @@ export function hydrateAuthImages(root) {
for (const e of entries) if (e.isIntersecting) { obs.unobserve(e.target); loadAuthImage(e.target); }
}, { rootMargin: '300px' });
}
-
- // Load every image now — the observer will also fire for them but
- // loadAuthImage is idempotent (deletes data-auth-src on first call).
- imgs.forEach(img => { loadAuthImage(img); _authImgObserver.observe(img); });
+ imgs.forEach(img => _authImgObserver.observe(img));
}
diff --git a/frontend/js/views/device-detail.js b/frontend/js/views/device-detail.js
index b617029..e4b57f5 100644
--- a/frontend/js/views/device-detail.js
+++ b/frontend/js/views/device-detail.js
@@ -1454,7 +1454,7 @@ async function setupPlaylistActions(device) {
`;
document.body.appendChild(modal);
- hydrateAuthImages(modal);
+ hydrateAuthImages(modal, { eager: true });
// Tab switching
modal.querySelectorAll('.assign-tab').forEach(tab => {
diff --git a/frontend/js/views/playlists.js b/frontend/js/views/playlists.js
index 3fbc36f..e3507a0 100644
--- a/frontend/js/views/playlists.js
+++ b/frontend/js/views/playlists.js
@@ -687,7 +687,7 @@ async function showAddItemModal(playlistId, opts = {}) {
`;
}).join('');
- hydrateAuthImages(list);
+ hydrateAuthImages(list, { eager: true });
list.querySelectorAll('.add-item-btn').forEach(btn => {
btn.addEventListener('click', async (e) => {
diff --git a/frontend/js/views/widgets.js b/frontend/js/views/widgets.js
index 8bae96c..413b7ca 100644
--- a/frontend/js/views/widgets.js
+++ b/frontend/js/views/widgets.js
@@ -74,7 +74,7 @@ function openContentPicker({ multiple = false, title } = {}) {
`;
}).join('')
}`;
- hydrateAuthImages(list);
+ hydrateAuthImages(list, { eager: true });
list.querySelectorAll('[data-pick-id]').forEach(el => el.onclick = () => {
const id = el.dataset.pickId;
if (multiple) {