diff --git a/css/style.css b/css/style.css index bae2bb11..de699c57 100644 --- a/css/style.css +++ b/css/style.css @@ -90,11 +90,29 @@ header img { } } +[data-video-thumbnail][data-thumbnail-loaded="true"] { + animation: video-thumbnail-fade-in 220ms ease-out forwards; +} + +@keyframes video-thumbnail-fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + @media (prefers-reduced-motion: reduce) { .video-card--enter { opacity: 1; animation: none; } + + [data-video-thumbnail][data-thumbnail-loaded="true"] { + animation: none; + opacity: 1; + } } .video-card:hover { diff --git a/js/app.js b/js/app.js index 064b8406..49cac9d1 100644 --- a/js/app.js +++ b/js/app.js @@ -2562,10 +2562,63 @@ class bitvidApp { if (cardEl) { const thumbnailEl = cardEl.querySelector("[data-video-thumbnail]"); if (thumbnailEl) { - const handleThumbnailLoad = () => { - if (!thumbnailEl.dataset.thumbnailFailed && escapedThumbnail) { - this.loadedThumbnails.set(video.id, escapedThumbnail); + const markThumbnailAsLoaded = () => { + if (!escapedThumbnail) { + return; } + + if (thumbnailEl.dataset.thumbnailLoaded !== "true") { + thumbnailEl.dataset.thumbnailLoaded = "true"; + } + + this.loadedThumbnails.set(video.id, escapedThumbnail); + }; + + const handleThumbnailLoad = () => { + if (thumbnailEl.dataset.thumbnailLoaded === "true") { + return; + } + + const hasPendingLazySrc = + typeof thumbnailEl.dataset.lazy === "string" && + thumbnailEl.dataset.lazy.trim().length > 0; + + if (hasPendingLazySrc) { + return; + } + + if (thumbnailEl.dataset.thumbnailFailed || !escapedThumbnail) { + return; + } + + const fallbackAttr = + (typeof thumbnailEl.dataset.fallbackSrc === "string" + ? thumbnailEl.dataset.fallbackSrc.trim() + : "") || + thumbnailEl.getAttribute("data-fallback-src") || + ""; + + const currentSrc = thumbnailEl.currentSrc || thumbnailEl.src || ""; + const isFallbackSrc = + !!fallbackAttr && + !!currentSrc && + (currentSrc === fallbackAttr || currentSrc.endsWith(fallbackAttr)); + + if (isFallbackSrc) { + return; + } + + if ( + (thumbnailEl.naturalWidth === 0 && + thumbnailEl.naturalHeight === 0) || + !currentSrc + ) { + return; + } + + markThumbnailAsLoaded(); + + thumbnailEl.removeEventListener("load", handleThumbnailLoad); }; const handleThumbnailError = () => { if ( @@ -2574,14 +2627,22 @@ class bitvidApp { ) { this.loadedThumbnails.delete(video.id); } + + if (thumbnailEl.dataset.thumbnailLoaded) { + delete thumbnailEl.dataset.thumbnailLoaded; + } + + thumbnailEl.removeEventListener("load", handleThumbnailLoad); }; - thumbnailEl.addEventListener("load", handleThumbnailLoad, { - once: true, - }); + thumbnailEl.addEventListener("load", handleThumbnailLoad); thumbnailEl.addEventListener("error", handleThumbnailError, { once: true, }); + + if (thumbnailEl.complete) { + handleThumbnailLoad(); + } } if (showUnsupportedTorrentBadge) {