Surface video details load failures in the info view

A failed details fetch for a video opened with list metadata set the
error state but never rendered it - the spinner cleared and the
description section silently disappeared. Show an inline error section
with a retry button and an error details sheet (copy/share, reusing the
player's ErrorDetailsSheet). Also fill in the empty video.error.title
localization.
This commit is contained in:
Arkadiusz Fal
2026-09-20 23:00:03 +02:00
parent 794b2b0181
commit eac5eb7778
2 changed files with 87 additions and 1 deletions

View File

@@ -19135,7 +19135,15 @@
}
},
"video.error.title" : {
"comment" : "Title shown when a video fails to load",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Failed to Load Video"
}
}
}
},
"video.navigation.next" : {
"comment" : "Accessibility label for next video button",

View File

@@ -41,6 +41,7 @@ struct VideoInfoView: View {
@State private var loadMoreError: String?
@State private var isBookmarked = false
@State private var showingErrorDetailsSheet = false
@State private var showingPlaylistSheet = false
@State private var showingCommentsSheet = false
@State private var showingRemoveBookmarkAlert = false
@@ -183,6 +184,15 @@ struct VideoInfoView: View {
return !description.isEmpty
}
/// Whether to surface a failed details fetch inline. The card still
/// renders from list metadata, but description/stats/related never
/// arrived — without this the sections just silently disappear.
private var shouldShowDetailsErrorSection: Bool {
guard !isLoadingVideoDetails, videoDetailsLoadError != nil,
let base = baseVideo else { return false }
return loadedVideoDetails[base.id.videoID] == nil
}
/// Whether the stats section should be visible.
/// Duration alone does not warrant showing stats.
private var shouldShowStatsSection: Bool {
@@ -382,6 +392,11 @@ struct VideoInfoView: View {
PlaylistSelectorSheet(video: video)
}
}
#if !os(tvOS)
.sheet(isPresented: $showingErrorDetailsSheet) {
ErrorDetailsSheet(errorMessage: videoDetailsLoadError ?? initialVideoLoadError ?? "Unknown error")
}
#endif
#if os(tvOS)
.fullScreenCover(isPresented: $showingCommentsSheet) {
commentsSheetContent
@@ -496,6 +511,15 @@ struct VideoInfoView: View {
bookmarkDetailsSection(bookmark)
}
// Details fetch failed - show error with retry instead of
// silently hiding the description/stats sections
if shouldShowDetailsErrorSection {
Divider()
.padding(.horizontal)
detailsErrorSection
}
// Description section (collapsible) - only show if description available
if shouldShowDescriptionSection {
Divider()
@@ -739,6 +763,12 @@ struct VideoInfoView: View {
.padding(.horizontal)
.padding(.vertical, 16)
}
} else if shouldShowDetailsErrorSection {
detailsErrorSection
Divider()
.padding(.horizontal)
.padding(.vertical, 16)
}
if !isDescriptionScrollLocked {
@@ -803,6 +833,14 @@ struct VideoInfoView: View {
await loadVideoDetails()
}
}
#if !os(tvOS)
Button {
showingErrorDetailsSheet = true
} label: {
Label(String(localized: "player.error.button"), systemImage: "info.circle")
}
#endif
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
@@ -1756,6 +1794,46 @@ struct VideoInfoView: View {
.frame(maxWidth: .infinity, alignment: .leading)
}
// MARK: - Details Error Section
/// Inline error shown when the details fetch failed but the video card
/// still renders from list metadata (e.g. deleted/unavailable video).
private var detailsErrorSection: some View {
VStack(spacing: 12) {
Label(String(localized: "video.error.title"), systemImage: "exclamationmark.triangle")
.font(.headline)
.foregroundStyle(.secondary)
if let error = videoDetailsLoadError {
Text(error)
.font(.subheadline)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
}
HStack(spacing: 12) {
Button(String(localized: "common.retry")) {
Task {
await loadVideoDetails()
}
}
.buttonStyle(.bordered)
#if !os(tvOS)
Button {
showingErrorDetailsSheet = true
} label: {
Label(String(localized: "player.error.button"), systemImage: "info.circle")
}
.buttonStyle(.bordered)
#endif
}
}
.frame(maxWidth: .infinity)
.padding(.horizontal)
.padding(.vertical, 16)
}
// MARK: - Description Section
private var descriptionSection: some View {