diff --git a/Yattee/Views/Channel/ChannelView.swift b/Yattee/Views/Channel/ChannelView.swift index d78b9847..105605a3 100644 --- a/Yattee/Views/Channel/ChannelView.swift +++ b/Yattee/Views/Channel/ChannelView.swift @@ -176,6 +176,12 @@ struct ChannelView: View { 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 /// Progress from 0 (fully expanded) to 1 (fully collapsed) @@ -1255,7 +1261,7 @@ struct ChannelView: View { rowStyle: rowStyle, listStyle: listStyle ) { - VideoRowView(video: video, style: rowStyle) + VideoRowView(video: video, style: rowStyle, watchProgress: watchProgress(for: video)) .tappableVideo( video, queueSource: videosQueueSource, @@ -1315,7 +1321,7 @@ struct ChannelView: View { private var videosGridContent: some View { VideoGridContent(columns: gridConfig.effectiveColumns) { 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( video, queueSource: videosQueueSource, @@ -1500,7 +1506,7 @@ struct ChannelView: View { rowStyle: rowStyle, listStyle: listStyle ) { - VideoRowView(video: video, style: rowStyle) + VideoRowView(video: video, style: rowStyle, watchProgress: watchProgress(for: video)) .tappableVideo( video, queueSource: shortsQueueSource, @@ -1540,7 +1546,7 @@ struct ChannelView: View { VideoGridContent(columns: gridConfig.effectiveColumns) { 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( video, queueSource: shortsQueueSource, @@ -1620,7 +1626,7 @@ struct ChannelView: View { rowStyle: rowStyle, listStyle: listStyle ) { - VideoRowView(video: video, style: rowStyle) + VideoRowView(video: video, style: rowStyle, watchProgress: watchProgress(for: video)) .tappableVideo( video, queueSource: streamsQueueSource, @@ -1660,7 +1666,7 @@ struct ChannelView: View { VideoGridContent(columns: gridConfig.effectiveColumns) { 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( video, queueSource: streamsQueueSource, @@ -1736,7 +1742,7 @@ struct ChannelView: View { ) { switch item { case .video(let video): - VideoRowView(video: video, style: rowStyle) + VideoRowView(video: video, style: rowStyle, watchProgress: watchProgress(for: video)) .tappableVideo( video, queueSource: searchQueueSource, @@ -1770,7 +1776,7 @@ struct ChannelView: View { ForEach(searchResults.items) { item in switch item { case .video(let video): - VideoCardView(video: video, isCompact: gridConfig.isCompactCards) + VideoCardView(video: video, watchProgress: watchProgress(for: video), isCompact: gridConfig.isCompactCards) .tappableVideo( video, queueSource: searchQueueSource, diff --git a/Yattee/Views/Playlist/PlaylistVideoRowView.swift b/Yattee/Views/Playlist/PlaylistVideoRowView.swift index 44601702..21f5f309 100644 --- a/Yattee/Views/Playlist/PlaylistVideoRowView.swift +++ b/Yattee/Views/Playlist/PlaylistVideoRowView.swift @@ -13,12 +13,14 @@ import SwiftUI struct PlaylistVideoRowView: View { let index: Int let video: Video + var watchProgress: Double? = nil var onRemove: (() -> Void)? = nil var body: some View { VideoRowView( video: video, style: .regular, + watchProgress: watchProgress, index: index ) .videoContextMenu( @@ -40,9 +42,10 @@ struct PlaylistVideoRowView: View { extension PlaylistVideoRowView { /// 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.video = item.toVideo() + self.watchProgress = watchProgress self.onRemove = onRemove } } diff --git a/Yattee/Views/Playlist/UnifiedPlaylistDetailView.swift b/Yattee/Views/Playlist/UnifiedPlaylistDetailView.swift index edc55517..37ef19d1 100644 --- a/Yattee/Views/Playlist/UnifiedPlaylistDetailView.swift +++ b/Yattee/Views/Playlist/UnifiedPlaylistDetailView.swift @@ -92,6 +92,8 @@ struct UnifiedPlaylistDetailView: View { @State private var hasLoadedDownloadState = false #endif + @State private var watchEntriesMap: [String: WatchEntry] = [:] + private var dataManager: DataManager? { appEnvironment?.dataManager } private var isQueueEnabled: Bool { appEnvironment?.settingsManager.queueEnabled ?? true } @@ -187,6 +189,9 @@ struct UnifiedPlaylistDetailView: View { .task { await loadPlaylist() } + .onReceive(NotificationCenter.default.publisher(for: .watchHistoryDidChange)) { _ in + loadWatchEntries() + } #if !os(tvOS) .batchDownload(coordinator: downloadCoordinator) .onAppear { @@ -663,6 +668,7 @@ struct UnifiedPlaylistDetailView: View { PlaylistVideoRowView( index: index + 1, video: video, + watchProgress: watchProgress(for: video), onRemove: isLocal ? { removeVideo(at: index) } : nil ) } @@ -697,6 +703,17 @@ struct UnifiedPlaylistDetailView: View { case .remote(let playlistID, let 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() { diff --git a/Yattee/Views/Search/SearchView.swift b/Yattee/Views/Search/SearchView.swift index 7ed6ce4d..5759b3da 100644 --- a/Yattee/Views/Search/SearchView.swift +++ b/Yattee/Views/Search/SearchView.swift @@ -64,6 +64,7 @@ struct SearchView: View { // Grid layout configuration @State private var viewWidth: CGFloat = 0 + @State private var watchEntriesMap: [String: WatchEntry] = [:] private var gridConfig: GridLayoutConfiguration { GridLayoutConfiguration(viewWidth: viewWidth, gridColumns: gridColumns) } @@ -152,6 +153,12 @@ struct SearchView: View { .onReceive(NotificationCenter.default.publisher(for: .searchHistoryDidChange)) { _ in loadSearchHistory() } + .onReceive(NotificationCenter.default.publisher(for: .watchHistoryDidChange)) { _ in + loadWatchEntries() + } + .task { + loadWatchEntries() + } .onReceive(NotificationCenter.default.publisher(for: .recentChannelsDidChange)) { _ in loadRecentChannels() } @@ -1110,7 +1117,7 @@ struct SearchView: View { ) { switch item { case .video(let video, let videoIndex): - VideoRowView(video: video, style: rowStyle) + VideoRowView(video: video, style: rowStyle, watchProgress: watchProgress(for: video)) .tappableVideo( video, queueSource: searchQueueSource, @@ -1181,6 +1188,7 @@ struct SearchView: View { case .video(let video, let videoIndex): VideoCardView( video: video, + watchProgress: watchProgress(for: video), isCompact: gridConfig.isCompactCards ) .frame(maxHeight: .infinity, alignment: .top) @@ -1252,6 +1260,16 @@ struct SearchView: View { // 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() { guard appEnvironment?.settingsManager.saveRecentSearches != false else { searchHistory = []