Fix feed channel filter avatars showing placeholders instead of images

The filter strip was passing the Invidious instance URL as serverURL to
AvatarURLBuilder, which built a Yattee Server-style /avatar/ path that
doesn't exist on Invidious. Now passes the actual Yattee Server URL
(matching SubscriptionsView pattern) and enriches channels from
CachedChannelData as a fallback when the API doesn't return thumbnails.
This commit is contained in:
Arkadiusz Fal
2026-02-13 07:04:40 +01:00
parent 9eb6812fb2
commit e51ebd7ab2
2 changed files with 37 additions and 4 deletions

View File

@@ -94,6 +94,27 @@ struct ChannelID: Codable, Hashable, Sendable {
} }
} }
extension Channel {
/// Returns a copy with `thumbnailURL` filled from cached channel data if currently nil.
@MainActor
func enrichedThumbnail(using dataManager: DataManager) -> Channel {
guard thumbnailURL == nil else { return self }
guard let cached = CachedChannelData.load(for: id.channelID, using: dataManager) else {
return self
}
return Channel(
id: id,
name: name,
description: description,
subscriberCount: subscriberCount ?? cached.subscriberCount,
videoCount: videoCount,
thumbnailURL: cached.thumbnailURL,
bannerURL: bannerURL ?? cached.bannerURL,
isVerified: isVerified
)
}
}
extension ChannelID: Identifiable { extension ChannelID: Identifiable {
var id: String { var id: String {
switch source { switch source {

View File

@@ -67,12 +67,24 @@ struct InstanceBrowseView: View {
appEnvironment?.settingsManager.listStyle ?? .inset appEnvironment?.settingsManager.listStyle ?? .inset
} }
/// Auth header for Yattee Server instances /// The first enabled Yattee Server instance (for avatar URLs).
private var yatteeServer: Instance? {
appEnvironment?.instancesManager.enabledYatteeServerInstances.first
}
private var yatteeServerURL: URL? { yatteeServer?.url }
/// Auth header for Yattee Server instances (when browsing a Yattee Server directly)
private var yatteeServerAuthHeader: String? { private var yatteeServerAuthHeader: String? {
guard instance.type == .yatteeServer else { return nil } guard instance.type == .yatteeServer else { return nil }
return appEnvironment?.yatteeServerCredentialsManager.basicAuthHeader(for: instance) return appEnvironment?.yatteeServerCredentialsManager.basicAuthHeader(for: instance)
} }
/// Auth header for avatar loading (uses Yattee Server for YouTube channel avatars)
private var avatarAuthHeader: String? {
guard let server = yatteeServer else { return nil }
return appEnvironment?.yatteeServerCredentialsManager.basicAuthHeader(for: server)
}
enum BrowseTab: String, CaseIterable, Identifiable { enum BrowseTab: String, CaseIterable, Identifiable {
case popular case popular
case trending case trending
@@ -425,7 +437,7 @@ struct InstanceBrowseView: View {
channelID: subscription.id.channelID, channelID: subscription.id.channelID,
name: subscription.name, name: subscription.name,
avatarURL: subscription.thumbnailURL, avatarURL: subscription.thumbnailURL,
serverURL: instance.url, serverURL: yatteeServerURL,
isSelected: selectedFeedChannelID == subscription.id.channelID, isSelected: selectedFeedChannelID == subscription.id.channelID,
avatarSize: 44, avatarSize: 44,
onTap: { onTap: {
@@ -441,7 +453,7 @@ struct InstanceBrowseView: View {
) )
}, },
onUnsubscribe: nil, onUnsubscribe: nil,
authHeader: yatteeServerAuthHeader authHeader: avatarAuthHeader
) )
} }
} }
@@ -929,7 +941,7 @@ struct InstanceBrowseView: View {
return return
} }
feedSubscriptions = subscriptionChannels feedSubscriptions = subscriptionChannels.map { $0.enrichedThumbnail(using: appEnvironment.dataManager) }
feedVideos = videos feedVideos = videos
prefetchBranding(for: videos) prefetchBranding(for: videos)
case .playlists: case .playlists: