Prefetch fresh video thumbnail before swapping it into info view

This commit is contained in:
Arkadiusz Fal
2026-04-23 18:37:19 +02:00
parent 6eb215f59c
commit fd0eab7784
2 changed files with 32 additions and 0 deletions

View File

@@ -81,6 +81,22 @@ final class ImageLoadingService: Sendable {
) )
} }
/// Warm the image cache for the given URL so a subsequent `LazyImage`
/// display hits the cache instantly. Returns after the image is cached
/// or the operation fails (silently). Bounded by `timeout` seconds.
nonisolated func prefetchImage(for url: URL, timeout: TimeInterval = 3) async {
await withTaskGroup(of: Void.self) { group in
group.addTask {
_ = try? await ImagePipeline.shared.image(for: url)
}
group.addTask {
try? await Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000))
}
await group.next()
group.cancelAll()
}
}
/// Remove a specific URL from both the memory and disk image caches. /// Remove a specific URL from both the memory and disk image caches.
/// Use when a previously cached URL is known to return stale or broken /// Use when a previously cached URL is known to return stale or broken
/// data (e.g. an expired proxied thumbnail URL). /// data (e.g. an expired proxied thumbnail URL).

View File

@@ -2165,6 +2165,7 @@ struct VideoInfoView: View {
do { do {
// Use extractURL method - just use the video part // Use extractURL method - just use the video part
let (fullVideo, _, _) = try await contentService.extractURL(originalURL, instance: instance) let (fullVideo, _, _) = try await contentService.extractURL(originalURL, instance: instance)
await prefetchNewThumbnailIfNeeded(old: base, new: fullVideo)
invalidateStaleThumbnails(old: base, new: fullVideo) invalidateStaleThumbnails(old: base, new: fullVideo)
loadedVideoDetails[videoID] = fullVideo loadedVideoDetails[videoID] = fullVideo
CachedChannelData.cacheAuthor(fullVideo.author) CachedChannelData.cacheAuthor(fullVideo.author)
@@ -2190,6 +2191,7 @@ struct VideoInfoView: View {
id: videoID, id: videoID,
instance: instance instance: instance
) )
await prefetchNewThumbnailIfNeeded(old: base, new: fullVideo)
invalidateStaleThumbnails(old: base, new: fullVideo) invalidateStaleThumbnails(old: base, new: fullVideo)
loadedVideoDetails[videoID] = fullVideo loadedVideoDetails[videoID] = fullVideo
CachedChannelData.cacheAuthor(fullVideo.author) CachedChannelData.cacheAuthor(fullVideo.author)
@@ -2200,6 +2202,20 @@ struct VideoInfoView: View {
isLoadingVideoDetails = false isLoadingVideoDetails = false
} }
/// Warms the Nuke cache for the freshly fetched thumbnail *before* we
/// swap it into the view. Without this, the view observes a URL change,
/// flips `LazyImage` to the new URL, and briefly shows a placeholder
/// while the download happens the "thumbnail flash". By the time the
/// swap lands, the image is already cached.
@MainActor
private func prefetchNewThumbnailIfNeeded(old: Video, new: Video) async {
guard let newURL = new.bestThumbnail?.url else { return }
let oldKey = old.bestThumbnail.map { ImageLoadingService.cacheKey(for: $0.url) }
let newKey = ImageLoadingService.cacheKey(for: newURL)
guard oldKey != newKey else { return }
await ImageLoadingService.shared.prefetchImage(for: newURL)
}
/// Evicts image cache entries for thumbnails whose URLs changed between /// Evicts image cache entries for thumbnails whose URLs changed between
/// the queued/list copy of the video and the freshly fetched details, so /// the queued/list copy of the video and the freshly fetched details, so
/// other views holding the old URLs re-fetch instead of reusing broken /// other views holding the old URLs re-fetch instead of reusing broken