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 { LazyVStack(spacing: DS.Spacing.cardGap) { ForEach(viewModel.highlights) { item in highlightCard(item) } } } } .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: { HStack(spacing: 16) { // Thumbnail 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 ) } Image(systemName: "play.circle.fill") .font(.system(size: playIconSize)) .foregroundStyle(.white.opacity(0.7)) .shadow(radius: 4) if item.isCondensedGame { VStack { HStack { Spacer() Text("CONDENSED") .font(badgeFont) .foregroundStyle(.white) .kerning(0.5) .padding(.horizontal, 8) .padding(.vertical, 4) .background(DS.Colors.media) .clipShape(Capsule()) } Spacer() } .padding(8) } } .frame(width: thumbnailWidth, height: thumbnailHeight) .clipShape(RoundedRectangle(cornerRadius: DS.Radii.compact)) // Info VStack(alignment: .leading, spacing: 6) { HStack { Text(item.gameTitle) .font(gameTagFont) .foregroundStyle(DS.Colors.textTertiary) .kerning(0.8) Spacer() Text(timeAgo(item.timestamp)) .font(gameTagFont) .foregroundStyle(DS.Colors.textQuaternary) } Text(item.headline) .font(headlineFont) .foregroundStyle(DS.Colors.textPrimary) .lineLimit(2) } Spacer(minLength: 0) } .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 private func timeAgo(_ date: Date) -> String { let interval = Date().timeIntervalSince(date) if interval < 60 { return "Just now" } if interval < 3600 { return "\(Int(interval / 60))m ago" } if interval < 86400 { return "\(Int(interval / 3600))h ago" } return "\(Int(interval / 86400))d ago" } #if os(tvOS) private var edgeInset: CGFloat { 60 } private var thumbnailWidth: CGFloat { 300 } private var thumbnailHeight: CGFloat { 160 } private var thumbnailLogoSize: CGFloat { 48 } private var thumbnailLogoGap: CGFloat { 20 } private var playIconSize: CGFloat { 40 } private var atFontSize: CGFloat { 22 } private var headlineFont: Font { .system(size: 24, weight: .semibold) } private var gameTagFont: Font { .system(size: 22, weight: .bold, design: .rounded) } private var badgeFont: Font { .system(size: 18, weight: .bold, design: .rounded) } #else private var edgeInset: CGFloat { 20 } private var thumbnailWidth: CGFloat { 180 } private var thumbnailHeight: CGFloat { 100 } private var thumbnailLogoSize: CGFloat { 32 } private var thumbnailLogoGap: CGFloat { 12 } private var playIconSize: CGFloat { 28 } private var atFontSize: CGFloat { 14 } private var headlineFont: Font { .system(size: 15, weight: .semibold) } private var gameTagFont: Font { .system(size: 12, weight: .bold, design: .rounded) } private var badgeFont: Font { .system(size: 11, weight: .bold, design: .rounded) } #endif }