mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-15 23:03:14 -06:00
#182 shipped hydrateAuthImages() loading every thumbnail immediately, which regressed the content-library grid from lazy to eager (a fetch per thumbnail on render) and left the IntersectionObserver as dead code. Restore lazy-by-default (observe-only) so large grids only fetch thumbnails as they scroll into view, and add an { eager: true } opt-in for the transient pickers where every item is on screen and immediate load reads better: the device assign-content modal, the playlist add-item modal, and the widget content picker. Grids and inline lists (content library, playlist items, device playlist tab, directory logo/background) use the lazy default. Behavior for those pickers is unchanged; only the large grids revert to the lazy loading they had before #182. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
00e8300af7
commit
84ad89b06d
|
|
@ -73,8 +73,8 @@ export function isPlatformAdmin(user) {
|
||||||
|
|
||||||
// Lazy-load authenticated images. A plain <img> can't send the Bearer token,
|
// Lazy-load authenticated images. A plain <img> can't send the Bearer token,
|
||||||
// and thumbnail/file endpoints require auth — a just-uploaded item's thumbnail
|
// 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.
|
// 403's without it. We fetch with the token and swap in an object URL, revoked
|
||||||
// IntersectionObserver keeps it lazy; the object URL is revoked after load.
|
// after load.
|
||||||
let _authImgObserver = null;
|
let _authImgObserver = null;
|
||||||
export function loadAuthImage(img) {
|
export function loadAuthImage(img) {
|
||||||
const url = img.dataset.authSrc;
|
const url = img.dataset.authSrc;
|
||||||
|
|
@ -89,13 +89,17 @@ export function loadAuthImage(img) {
|
||||||
})
|
})
|
||||||
.catch(() => { img.style.opacity = '0.25'; });
|
.catch(() => { img.style.opacity = '0.25'; });
|
||||||
}
|
}
|
||||||
export function hydrateAuthImages(root) {
|
// Hydrate <img data-auth-src> 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]');
|
const imgs = root.querySelectorAll('img[data-auth-src]');
|
||||||
if (!imgs.length) return;
|
if (!imgs.length) return;
|
||||||
|
|
||||||
// Load all images immediately; IntersectionObserver is used below
|
// Eager path (opt-in) and the no-IntersectionObserver fallback both load now.
|
||||||
// only for images that are off-screen (lazy loading).
|
if (eager || typeof IntersectionObserver === 'undefined') {
|
||||||
if (typeof IntersectionObserver === 'undefined') {
|
|
||||||
imgs.forEach(loadAuthImage);
|
imgs.forEach(loadAuthImage);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -105,8 +109,5 @@ export function hydrateAuthImages(root) {
|
||||||
for (const e of entries) if (e.isIntersecting) { obs.unobserve(e.target); loadAuthImage(e.target); }
|
for (const e of entries) if (e.isIntersecting) { obs.unobserve(e.target); loadAuthImage(e.target); }
|
||||||
}, { rootMargin: '300px' });
|
}, { rootMargin: '300px' });
|
||||||
}
|
}
|
||||||
|
imgs.forEach(img => _authImgObserver.observe(img));
|
||||||
// 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); });
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1454,7 +1454,7 @@ async function setupPlaylistActions(device) {
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
document.body.appendChild(modal);
|
document.body.appendChild(modal);
|
||||||
hydrateAuthImages(modal);
|
hydrateAuthImages(modal, { eager: true });
|
||||||
|
|
||||||
// Tab switching
|
// Tab switching
|
||||||
modal.querySelectorAll('.assign-tab').forEach(tab => {
|
modal.querySelectorAll('.assign-tab').forEach(tab => {
|
||||||
|
|
|
||||||
|
|
@ -687,7 +687,7 @@ async function showAddItemModal(playlistId, opts = {}) {
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}).join('');
|
}).join('');
|
||||||
hydrateAuthImages(list);
|
hydrateAuthImages(list, { eager: true });
|
||||||
|
|
||||||
list.querySelectorAll('.add-item-btn').forEach(btn => {
|
list.querySelectorAll('.add-item-btn').forEach(btn => {
|
||||||
btn.addEventListener('click', async (e) => {
|
btn.addEventListener('click', async (e) => {
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ function openContentPicker({ multiple = false, title } = {}) {
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join('')
|
}).join('')
|
||||||
}</div>`;
|
}</div>`;
|
||||||
hydrateAuthImages(list);
|
hydrateAuthImages(list, { eager: true });
|
||||||
list.querySelectorAll('[data-pick-id]').forEach(el => el.onclick = () => {
|
list.querySelectorAll('[data-pick-id]').forEach(el => el.onclick = () => {
|
||||||
const id = el.dataset.pickId;
|
const id = el.dataset.pickId;
|
||||||
if (multiple) {
|
if (multiple) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue