Show watch progress bar on thumbnails in playlist, channel, and search views

These views rendered video thumbnails without passing watchProgress, so the
progress bar was silently missing. Apply the existing pattern from
SubscriptionsView: maintain a watchEntriesMap and forward watchProgress(for:)
to VideoRowView/VideoCardView at each call site.
This commit is contained in:
Arkadiusz Fal
2026-05-08 20:58:13 +02:00
parent 5ab9e3d5bf
commit 7c1549ed35
4 changed files with 54 additions and 10 deletions

View File

@@ -176,6 +176,12 @@ struct ChannelView: View {
watchEntriesMap = appEnvironment?.dataManager.watchEntriesMap() ?? [:] watchEntriesMap = appEnvironment?.dataManager.watchEntriesMap() ?? [:]
} }
private func watchProgress(for video: Video) -> Double? {
guard let entry = watchEntriesMap[video.id.videoID] else { return nil }
let progress = entry.progress
return progress > 0 && progress < 1 ? progress : nil
}
// MARK: - Computed Properties for Scroll Animation // MARK: - Computed Properties for Scroll Animation
/// Progress from 0 (fully expanded) to 1 (fully collapsed) /// Progress from 0 (fully expanded) to 1 (fully collapsed)
@@ -1255,7 +1261,7 @@ struct ChannelView: View {
rowStyle: rowStyle, rowStyle: rowStyle,
listStyle: listStyle listStyle: listStyle
) { ) {
VideoRowView(video: video, style: rowStyle) VideoRowView(video: video, style: rowStyle, watchProgress: watchProgress(for: video))
.tappableVideo( .tappableVideo(
video, video,
queueSource: videosQueueSource, queueSource: videosQueueSource,
@@ -1315,7 +1321,7 @@ struct ChannelView: View {
private var videosGridContent: some View { private var videosGridContent: some View {
VideoGridContent(columns: gridConfig.effectiveColumns) { VideoGridContent(columns: gridConfig.effectiveColumns) {
ForEach(Array(filteredVideos.enumerated()), id: \.element.id) { index, video in ForEach(Array(filteredVideos.enumerated()), id: \.element.id) { index, video in
VideoCardView(video: video, isCompact: gridConfig.isCompactCards) VideoCardView(video: video, watchProgress: watchProgress(for: video), isCompact: gridConfig.isCompactCards)
.tappableVideo( .tappableVideo(
video, video,
queueSource: videosQueueSource, queueSource: videosQueueSource,
@@ -1500,7 +1506,7 @@ struct ChannelView: View {
rowStyle: rowStyle, rowStyle: rowStyle,
listStyle: listStyle listStyle: listStyle
) { ) {
VideoRowView(video: video, style: rowStyle) VideoRowView(video: video, style: rowStyle, watchProgress: watchProgress(for: video))
.tappableVideo( .tappableVideo(
video, video,
queueSource: shortsQueueSource, queueSource: shortsQueueSource,
@@ -1540,7 +1546,7 @@ struct ChannelView: View {
VideoGridContent(columns: gridConfig.effectiveColumns) { VideoGridContent(columns: gridConfig.effectiveColumns) {
ForEach(Array(filteredShorts.enumerated()), id: \.element.id) { index, video in ForEach(Array(filteredShorts.enumerated()), id: \.element.id) { index, video in
VideoCardView(video: video, isCompact: gridConfig.isCompactCards) VideoCardView(video: video, watchProgress: watchProgress(for: video), isCompact: gridConfig.isCompactCards)
.tappableVideo( .tappableVideo(
video, video,
queueSource: shortsQueueSource, queueSource: shortsQueueSource,
@@ -1620,7 +1626,7 @@ struct ChannelView: View {
rowStyle: rowStyle, rowStyle: rowStyle,
listStyle: listStyle listStyle: listStyle
) { ) {
VideoRowView(video: video, style: rowStyle) VideoRowView(video: video, style: rowStyle, watchProgress: watchProgress(for: video))
.tappableVideo( .tappableVideo(
video, video,
queueSource: streamsQueueSource, queueSource: streamsQueueSource,
@@ -1660,7 +1666,7 @@ struct ChannelView: View {
VideoGridContent(columns: gridConfig.effectiveColumns) { VideoGridContent(columns: gridConfig.effectiveColumns) {
ForEach(Array(filteredStreams.enumerated()), id: \.element.id) { index, video in ForEach(Array(filteredStreams.enumerated()), id: \.element.id) { index, video in
VideoCardView(video: video, isCompact: gridConfig.isCompactCards) VideoCardView(video: video, watchProgress: watchProgress(for: video), isCompact: gridConfig.isCompactCards)
.tappableVideo( .tappableVideo(
video, video,
queueSource: streamsQueueSource, queueSource: streamsQueueSource,
@@ -1736,7 +1742,7 @@ struct ChannelView: View {
) { ) {
switch item { switch item {
case .video(let video): case .video(let video):
VideoRowView(video: video, style: rowStyle) VideoRowView(video: video, style: rowStyle, watchProgress: watchProgress(for: video))
.tappableVideo( .tappableVideo(
video, video,
queueSource: searchQueueSource, queueSource: searchQueueSource,
@@ -1770,7 +1776,7 @@ struct ChannelView: View {
ForEach(searchResults.items) { item in ForEach(searchResults.items) { item in
switch item { switch item {
case .video(let video): case .video(let video):
VideoCardView(video: video, isCompact: gridConfig.isCompactCards) VideoCardView(video: video, watchProgress: watchProgress(for: video), isCompact: gridConfig.isCompactCards)
.tappableVideo( .tappableVideo(
video, video,
queueSource: searchQueueSource, queueSource: searchQueueSource,

View File

@@ -13,12 +13,14 @@ import SwiftUI
struct PlaylistVideoRowView: View { struct PlaylistVideoRowView: View {
let index: Int let index: Int
let video: Video let video: Video
var watchProgress: Double? = nil
var onRemove: (() -> Void)? = nil var onRemove: (() -> Void)? = nil
var body: some View { var body: some View {
VideoRowView( VideoRowView(
video: video, video: video,
style: .regular, style: .regular,
watchProgress: watchProgress,
index: index index: index
) )
.videoContextMenu( .videoContextMenu(
@@ -40,9 +42,10 @@ struct PlaylistVideoRowView: View {
extension PlaylistVideoRowView { extension PlaylistVideoRowView {
/// Initialize from a LocalPlaylistItem model. /// Initialize from a LocalPlaylistItem model.
init(item: LocalPlaylistItem, index: Int, onRemove: @escaping () -> Void) { init(item: LocalPlaylistItem, index: Int, watchProgress: Double? = nil, onRemove: @escaping () -> Void) {
self.index = index self.index = index
self.video = item.toVideo() self.video = item.toVideo()
self.watchProgress = watchProgress
self.onRemove = onRemove self.onRemove = onRemove
} }
} }

View File

@@ -92,6 +92,8 @@ struct UnifiedPlaylistDetailView: View {
@State private var hasLoadedDownloadState = false @State private var hasLoadedDownloadState = false
#endif #endif
@State private var watchEntriesMap: [String: WatchEntry] = [:]
private var dataManager: DataManager? { appEnvironment?.dataManager } private var dataManager: DataManager? { appEnvironment?.dataManager }
private var isQueueEnabled: Bool { appEnvironment?.settingsManager.queueEnabled ?? true } private var isQueueEnabled: Bool { appEnvironment?.settingsManager.queueEnabled ?? true }
@@ -187,6 +189,9 @@ struct UnifiedPlaylistDetailView: View {
.task { .task {
await loadPlaylist() await loadPlaylist()
} }
.onReceive(NotificationCenter.default.publisher(for: .watchHistoryDidChange)) { _ in
loadWatchEntries()
}
#if !os(tvOS) #if !os(tvOS)
.batchDownload(coordinator: downloadCoordinator) .batchDownload(coordinator: downloadCoordinator)
.onAppear { .onAppear {
@@ -663,6 +668,7 @@ struct UnifiedPlaylistDetailView: View {
PlaylistVideoRowView( PlaylistVideoRowView(
index: index + 1, index: index + 1,
video: video, video: video,
watchProgress: watchProgress(for: video),
onRemove: isLocal ? { removeVideo(at: index) } : nil onRemove: isLocal ? { removeVideo(at: index) } : nil
) )
} }
@@ -697,6 +703,17 @@ struct UnifiedPlaylistDetailView: View {
case .remote(let playlistID, let instance, _): case .remote(let playlistID, let instance, _):
await loadRemotePlaylist(playlistID: playlistID, instance: instance) await loadRemotePlaylist(playlistID: playlistID, instance: instance)
} }
loadWatchEntries()
}
private func loadWatchEntries() {
watchEntriesMap = dataManager?.watchEntriesMap() ?? [:]
}
private func watchProgress(for video: Video) -> Double? {
guard let entry = watchEntriesMap[video.id.videoID] else { return nil }
let progress = entry.progress
return progress > 0 && progress < 1 ? progress : nil
} }
private func loadLocalPlaylist() { private func loadLocalPlaylist() {

View File

@@ -64,6 +64,7 @@ struct SearchView: View {
// Grid layout configuration // Grid layout configuration
@State private var viewWidth: CGFloat = 0 @State private var viewWidth: CGFloat = 0
@State private var watchEntriesMap: [String: WatchEntry] = [:]
private var gridConfig: GridLayoutConfiguration { private var gridConfig: GridLayoutConfiguration {
GridLayoutConfiguration(viewWidth: viewWidth, gridColumns: gridColumns) GridLayoutConfiguration(viewWidth: viewWidth, gridColumns: gridColumns)
} }
@@ -152,6 +153,12 @@ struct SearchView: View {
.onReceive(NotificationCenter.default.publisher(for: .searchHistoryDidChange)) { _ in .onReceive(NotificationCenter.default.publisher(for: .searchHistoryDidChange)) { _ in
loadSearchHistory() loadSearchHistory()
} }
.onReceive(NotificationCenter.default.publisher(for: .watchHistoryDidChange)) { _ in
loadWatchEntries()
}
.task {
loadWatchEntries()
}
.onReceive(NotificationCenter.default.publisher(for: .recentChannelsDidChange)) { _ in .onReceive(NotificationCenter.default.publisher(for: .recentChannelsDidChange)) { _ in
loadRecentChannels() loadRecentChannels()
} }
@@ -1110,7 +1117,7 @@ struct SearchView: View {
) { ) {
switch item { switch item {
case .video(let video, let videoIndex): case .video(let video, let videoIndex):
VideoRowView(video: video, style: rowStyle) VideoRowView(video: video, style: rowStyle, watchProgress: watchProgress(for: video))
.tappableVideo( .tappableVideo(
video, video,
queueSource: searchQueueSource, queueSource: searchQueueSource,
@@ -1181,6 +1188,7 @@ struct SearchView: View {
case .video(let video, let videoIndex): case .video(let video, let videoIndex):
VideoCardView( VideoCardView(
video: video, video: video,
watchProgress: watchProgress(for: video),
isCompact: gridConfig.isCompactCards isCompact: gridConfig.isCompactCards
) )
.frame(maxHeight: .infinity, alignment: .top) .frame(maxHeight: .infinity, alignment: .top)
@@ -1252,6 +1260,16 @@ struct SearchView: View {
// MARK: - Search History Helpers // MARK: - Search History Helpers
private func loadWatchEntries() {
watchEntriesMap = appEnvironment?.dataManager.watchEntriesMap() ?? [:]
}
private func watchProgress(for video: Video) -> Double? {
guard let entry = watchEntriesMap[video.id.videoID] else { return nil }
let progress = entry.progress
return progress > 0 && progress < 1 ? progress : nil
}
private func loadSearchHistory() { private func loadSearchHistory() {
guard appEnvironment?.settingsManager.saveRecentSearches != false else { guard appEnvironment?.settingsManager.saveRecentSearches != false else {
searchHistory = [] searchHistory = []