// // QueueListView.swift // Yattee // // Shared queue list (history / now playing / up next) used by the queue // management sheet, with playback actions reusable by the tvOS queue panel. // import SwiftUI // MARK: - Playback Actions /// Shared queue playback actions used by `QueueListView` and the tvOS queue panel. @MainActor enum QueuePlaybackActions { /// Plays the queue item at `index`, pushing the current video and any /// skipped items to history. static func playVideo(at index: Int, appEnvironment: AppEnvironment?) { guard let appEnvironment else { return } let playerService = appEnvironment.playerService let playerState = playerService.state let queueManager = appEnvironment.queueManager let settingsManager = appEnvironment.settingsManager let queue = playerState.queue guard index < queue.count else { return } let queuedVideo = queue[index] // Push current video to history before switching (skip if incognito or history disabled) if let currentVideo = playerState.currentVideo, !settingsManager.incognitoModeEnabled, settingsManager.saveWatchHistory { let currentItem = QueuedVideo( video: currentVideo, stream: playerState.currentStream, audioStream: playerState.currentAudioStream, startTime: playerState.currentTime ) playerState.pushToHistory(currentItem) } // Remove played video and all before it from queue, pushing them to history for _ in 0.. Void)? = nil /// When false, hides the previously-played section (queue page setting). var showHistory: Bool = true private var queueManager: QueueManager? { appEnvironment?.queueManager } private var playerState: PlayerState? { appEnvironment?.playerService.state } private var queue: [QueuedVideo] { playerState?.queue ?? [] } private var history: [QueuedVideo] { playerState?.history ?? [] } private var isLoadingMore: Bool { queueManager?.isLoadingMore ?? false } private var hasMoreItems: Bool { queueManager?.hasMoreItems() ?? false } private var listStyle: VideoListStyle { appEnvironment?.settingsManager.listStyle ?? .inset } var body: some View { ScrollViewReader { proxy in List { // History section - show previously played videos if showHistory, !history.isEmpty { Section { ForEach(Array(history.enumerated()), id: \.element.id) { index, historyItem in QueueItemRow( queuedVideo: historyItem, index: nil, // History items don't show numbers isCurrentlyPlaying: false, onRemove: { }, onTap: { QueuePlaybackActions.playFromHistory(at: index, appEnvironment: appEnvironment) onPlay?() } ) } } header: { HStack { Text(String(localized: "queue.section.previously")) Spacer() Text(String(localized: "queue.section.count \(history.count)")) .font(.caption) .foregroundStyle(.secondary) } } } // Now Playing section - show the currently playing video from playerState if let currentVideo = playerState?.currentVideo { Section { // Create a temporary QueuedVideo for display let nowPlayingItem = QueuedVideo( video: currentVideo, stream: playerState?.currentStream, queueSource: nil ) QueueItemRow( queuedVideo: nowPlayingItem, index: nil, // Now playing shows waveform, not a number isCurrentlyPlaying: true, onRemove: { }, onTap: { } ) } header: { Text(String(localized: "queue.section.nowPlaying")) } .id("now-playing") } // Up Next section - show all queued videos if !queue.isEmpty { Section { ForEach(Array(queue.enumerated()), id: \.element.id) { index, queuedVideo in QueueItemRow( queuedVideo: queuedVideo, index: index + 1, // +1 because "Now Playing" is shown as position 1 isCurrentlyPlaying: false, onRemove: { withAnimation { queueManager?.removeFromQueue(id: queuedVideo.id) } }, onTap: { QueuePlaybackActions.playVideo(at: index, appEnvironment: appEnvironment) onPlay?() } ) } .onMove { source, destination in guard let fromIndex = source.first else { return } queueManager?.moveQueueItem(from: fromIndex, to: destination) } // Load more indicator if hasMoreItems { HStack { Spacer() if isLoadingMore { ProgressView() .controlSize(.small) } else { Button { Task { try? await queueManager?.loadMoreQueueItems() } } label: { Text(String(localized: "queue.sheet.loadMore")) .font(.subheadline) .foregroundStyle(.tint) } } Spacer() } .listRowBackground(Color.clear) } } header: { HStack { Text(String(localized: "queue.section.upNext")) Spacer() Text(String(localized: "queue.section.count \(queue.count)")) .font(.caption) .foregroundStyle(.secondary) } } } } #if os(iOS) .if(listStyle == .plain, then: { view in view.listStyle(.plain) }, else: { view in view.listStyle(.insetGrouped) }) #endif .onAppear { // Scroll to now playing section (no animation for instant positioning) DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { proxy.scrollTo("now-playing", anchor: .center) } } } } } #endif