Surface video detail load failures in the error view for placeholder videos

Opening a members-only video from a history or feed row got a 500 from
the videos API and left the detail view blank: the retry error view only
covered the videoID init mode, while the details fetch for a loaded
video failed silently.

Record the details-fetch error and route it into the same error view
when the displayed video is a bare placeholder (no author, no
thumbnails), show a spinner while that fetch is in flight, and make
retry re-run both loaders. Videos with real partial metadata keep
rendering as before.
This commit is contained in:
Arkadiusz Fal
2026-09-17 23:34:15 +02:00
parent 101880d35f
commit 93886b20ea

View File

@@ -108,6 +108,7 @@ struct VideoInfoView: View {
// Video details cache - stores full video details loaded from API
@State private var loadedVideoDetails: [String: Video] = [:]
@State private var isLoadingVideoDetails = false
@State private var videoDetailsLoadError: String?
/// Combined video list including originally loaded videos and extended videos from continuation
private var allVideos: [Video]? {
guard let originalList = videoQueueContext?.videoList else { return nil }
@@ -133,6 +134,14 @@ struct VideoInfoView: View {
return loadedVideoDetails[base.id.videoID] ?? base
}
/// Whether the displayed video is only an ID standing in for metadata
/// (a server-history row whose hydration failed), so a failed details
/// fetch leaves nothing worth rendering.
private var displayedVideoIsPlaceholder: Bool {
guard let video = displayedVideo else { return false }
return video.author.name.isEmpty && video.thumbnails.isEmpty
}
private var accentColor: Color {
appEnvironment?.settingsManager.resolvedAccentColor ?? .accentColor
}
@@ -251,11 +260,13 @@ struct VideoInfoView: View {
var body: some View {
Group {
if isLoadingInitialVideo {
if isLoadingInitialVideo || (displayedVideoIsPlaceholder && isLoadingVideoDetails) {
ProgressView()
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else if let error = initialVideoLoadError {
videoLoadErrorView(error)
} else if let error = videoDetailsLoadError, displayedVideoIsPlaceholder {
videoLoadErrorView(error)
} else if displayedVideo != nil {
videoContent
}
@@ -326,6 +337,7 @@ struct VideoInfoView: View {
comments = []
commentsState = .idle
commentsContinuation = nil
videoDetailsLoadError = nil
scrollViewID = UUID() // Reset scroll position
// Don't reset scrollOffset here - let the new scroll view report its geometry
// Resetting to 0 causes the blur to jump to an incorrect position briefly
@@ -775,7 +787,9 @@ struct VideoInfoView: View {
}
#endif
/// Error view shown when video fails to load (videoID init mode only).
/// Error view shown when the video fails to load — either the initial
/// fetch in videoID mode, or the details fetch for a placeholder video
/// that has no metadata of its own to fall back on.
@ViewBuilder
private func videoLoadErrorView(_ message: String) -> some View {
ContentUnavailableView {
@@ -786,6 +800,7 @@ struct VideoInfoView: View {
Button(String(localized: "common.retry")) {
Task {
await loadInitialVideoIfNeeded()
await loadVideoDetails()
}
}
}
@@ -2168,6 +2183,7 @@ struct VideoInfoView: View {
}
isLoadingVideoDetails = true
videoDetailsLoadError = nil
do {
// Use extractURL method - just use the video part
let (fullVideo, _, _) = try await contentService.extractURL(originalURL, instance: instance)
@@ -2176,7 +2192,9 @@ struct VideoInfoView: View {
loadedVideoDetails[videoID] = fullVideo
CachedChannelData.cacheAuthor(fullVideo.author)
} catch {
// Fail silently - use partial video data we have
// Use the partial video data we have; the body surfaces the
// error only when the video is a bare placeholder.
videoDetailsLoadError = error.localizedDescription
}
isLoadingVideoDetails = false
return
@@ -2191,6 +2209,7 @@ struct VideoInfoView: View {
}
isLoadingVideoDetails = true
videoDetailsLoadError = nil
do {
let fullVideo = try await contentService.video(
@@ -2202,7 +2221,9 @@ struct VideoInfoView: View {
loadedVideoDetails[videoID] = fullVideo
CachedChannelData.cacheAuthor(fullVideo.author)
} catch {
// Fail silently - just use the partial video data we have
// Use the partial video data we have; the body surfaces the
// error only when the video is a bare placeholder.
videoDetailsLoadError = error.localizedDescription
}
isLoadingVideoDetails = false