diff --git a/frontend/js/utils.js b/frontend/js/utils.js
index 29dbdf1..da4d7cd 100644
--- a/frontend/js/utils.js
+++ b/frontend/js/utils.js
@@ -70,3 +70,43 @@ export function livenessBadge(data, opts = {}) {
export function isPlatformAdmin(user) {
return !!(user && (user.role === 'superadmin' || user.role === 'platform_admin'));
}
+
+// 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.
+let _authImgObserver = null;
+export function loadAuthImage(img) {
+ const url = img.dataset.authSrc;
+ if (!url) return;
+ delete img.dataset.authSrc;
+ fetch(url, { headers: { Authorization: `Bearer ${localStorage.getItem('token')}` } })
+ .then(r => (r.ok ? r.blob() : Promise.reject(r.status)))
+ .then(blob => {
+ const obj = URL.createObjectURL(blob);
+ img.addEventListener('load', () => URL.revokeObjectURL(obj), { once: true });
+ img.src = obj;
+ })
+ .catch(() => { img.style.opacity = '0.25'; });
+}
+export function hydrateAuthImages(root) {
+ 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') {
+ imgs.forEach(loadAuthImage);
+ return;
+ }
+
+ if (!_authImgObserver) {
+ _authImgObserver = new IntersectionObserver((entries, obs) => {
+ 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); });
+}
diff --git a/frontend/js/views/content-library.js b/frontend/js/views/content-library.js
index edb13be..6452484 100644
--- a/frontend/js/views/content-library.js
+++ b/frontend/js/views/content-library.js
@@ -1,6 +1,6 @@
import { api } from '../api.js';
import { showToast } from '../components/toast.js';
-import { esc } from '../utils.js';
+import { esc, hydrateAuthImages } from '../utils.js';
import { t } from '../i18n.js';
function formatFileSize(bytes) {
@@ -34,36 +34,6 @@ function toLocalDatetimeInput(epochSec) {
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}T${p(d.getHours())}:${p(d.getMinutes())}`;
}
-// Lazy-load authenticated thumbnails/previews. A plain
can't send the
-// Bearer token, and the content thumbnail/file endpoints require auth (or a
-// playlist/widget reference) - so a just-uploaded item's thumbnail 403'd. We fetch
-// with the token and swap in an object URL. IntersectionObserver keeps it lazy so
-// we stay under the /api/content rate limit; the object URL is revoked after load.
-let _authImgObserver = null;
-function loadAuthImage(img) {
- const url = img.dataset.authSrc;
- if (!url) return;
- delete img.dataset.authSrc;
- fetch(url, { headers: { Authorization: `Bearer ${localStorage.getItem('token')}` } })
- .then(r => (r.ok ? r.blob() : Promise.reject(r.status)))
- .then(blob => {
- const obj = URL.createObjectURL(blob);
- img.addEventListener('load', () => URL.revokeObjectURL(obj), { once: true });
- img.src = obj;
- })
- .catch(() => { img.style.opacity = '0.25'; });
-}
-function hydrateAuthImages(root) {
- const imgs = root.querySelectorAll('img[data-auth-src]');
- if (typeof IntersectionObserver === 'undefined') { imgs.forEach(loadAuthImage); return; }
- if (!_authImgObserver) {
- _authImgObserver = new IntersectionObserver((entries, obs) => {
- for (const e of entries) if (e.isIntersecting) { obs.unobserve(e.target); loadAuthImage(e.target); }
- }, { rootMargin: '300px' });
- }
- imgs.forEach(img => _authImgObserver.observe(img));
-}
-
export function render(container) {
container.innerHTML = `