import AVKit import SwiftUI struct FeedView: View { @Environment(GamesViewModel.self) private var gamesViewModel @State private var viewModel = FeedViewModel() @State private var playingURL: URL? var body: some View { ScrollView { VStack(alignment: .leading, spacing: DS.Spacing.sectionGap) { // Header HStack { VStack(alignment: .leading, spacing: 4) { Text("HIGHLIGHTS") .font(DS.Fonts.caption) .foregroundStyle(DS.Colors.textQuaternary) .kerning(3) Text("Across the League") #if os(tvOS) .font(DS.Fonts.tvSectionTitle) #else .font(DS.Fonts.sectionTitle) #endif .foregroundStyle(DS.Colors.textPrimary) } Spacer() if viewModel.isLoading { ProgressView() } } if viewModel.highlights.isEmpty && !viewModel.isLoading { emptyState } else { // All highlights in one horizontal scroll, ordered by time ScrollView(.horizontal) { LazyHStack(spacing: DS.Spacing.cardGap) { ForEach(viewModel.highlights) { item in highlightCard(item) .frame(width: cardWidth) } } .padding(.vertical, 8) } .platformFocusSection() .scrollClipDisabled() } } .padding(.horizontal, edgeInset) .padding(.vertical, DS.Spacing.sectionGap) } .task { await viewModel.loadHighlights(games: gamesViewModel.games) } .onChange(of: gamesViewModel.games.count) { Task { await viewModel.loadHighlights(games: gamesViewModel.games) } } .onAppear { viewModel.startAutoRefresh(games: gamesViewModel.games) } .onDisappear { viewModel.stopAutoRefresh() } .fullScreenCover(isPresented: Binding( get: { playingURL != nil }, set: { if !$0 { playingURL = nil } } )) { if let url = playingURL { let player = AVPlayer(url: url) VideoPlayer(player: player) .ignoresSafeArea() .onAppear { player.play() } .onDisappear { player.pause() } } } } @ViewBuilder private func highlightCard(_ item: HighlightItem) -> some View { Button { playingURL = item.hlsURL ?? item.mp4URL } label: { VStack(alignment: .leading, spacing: 10) { // Thumbnail area with team colors ZStack { HStack(spacing: 0) { Rectangle().fill(TeamAssets.color(for: item.awayCode).opacity(0.3)) Rectangle().fill(TeamAssets.color(for: item.homeCode).opacity(0.3)) } HStack(spacing: thumbnailLogoGap) { TeamLogoView( team: TeamInfo(code: item.awayCode, name: "", score: nil), size: thumbnailLogoSize ) Text("@") .font(.system(size: atFontSize, weight: .bold)) .foregroundStyle(DS.Colors.textTertiary) TeamLogoView( team: TeamInfo(code: item.homeCode, name: "", score: nil), size: thumbnailLogoSize ) } // Play icon overlay Image(systemName: "play.circle.fill") .font(.system(size: playIconSize)) .foregroundStyle(.white.opacity(0.8)) .shadow(radius: 4) // Condensed/Recap badge if item.isCondensedGame { VStack { HStack { Spacer() Text("CONDENSED") .font(DS.Fonts.caption) .foregroundStyle(.white) .kerning(0.8) .padding(.horizontal, 8) .padding(.vertical, 4) .background(DS.Colors.media) .clipShape(Capsule()) } Spacer() } .padding(8) } } .frame(height: thumbnailHeight) .clipShape(RoundedRectangle(cornerRadius: DS.Radii.compact)) // Info VStack(alignment: .leading, spacing: 4) { Text(item.gameTitle) .font(DS.Fonts.caption) .foregroundStyle(DS.Colors.textTertiary) .kerning(1) Text(item.headline) .font(headlineFont) .foregroundStyle(DS.Colors.textPrimary) .lineLimit(2) } .padding(.horizontal, 4) } .padding(DS.Spacing.panelPadCompact) .background(DS.Colors.panelFill) .clipShape(RoundedRectangle(cornerRadius: DS.Radii.standard)) .overlay( RoundedRectangle(cornerRadius: DS.Radii.standard) .strokeBorder(DS.Colors.panelStroke, lineWidth: 0.5) ) } .platformCardStyle() } private var emptyState: some View { VStack(spacing: 16) { Image(systemName: "play.rectangle.on.rectangle") .font(.system(size: 44)) .foregroundStyle(DS.Colors.textQuaternary) Text("No highlights available yet") .font(DS.Fonts.body) .foregroundStyle(DS.Colors.textTertiary) Text("Highlights appear as games are played") .font(DS.Fonts.bodySmall) .foregroundStyle(DS.Colors.textQuaternary) } .frame(maxWidth: .infinity) .padding(.top, 80) } // MARK: - Platform sizing #if os(tvOS) private var edgeInset: CGFloat { 60 } private var cardWidth: CGFloat { 420 } private var thumbnailHeight: CGFloat { 200 } private var thumbnailLogoSize: CGFloat { 56 } private var thumbnailLogoGap: CGFloat { 24 } private var playIconSize: CGFloat { 44 } private var atFontSize: CGFloat { 20 } private var headlineFont: Font { .system(size: 18, weight: .semibold) } #else private var edgeInset: CGFloat { 20 } private var cardWidth: CGFloat { 280 } private var thumbnailHeight: CGFloat { 140 } private var thumbnailLogoSize: CGFloat { 40 } private var thumbnailLogoGap: CGFloat { 16 } private var playIconSize: CGFloat { 32 } private var atFontSize: CGFloat { 15 } private var headlineFont: Font { .system(size: 15, weight: .semibold) } #endif }