From 39092e5f3d59a9b7c9183731f3295636f389785a Mon Sep 17 00:00:00 2001 From: Trey t Date: Sun, 12 Apr 2026 13:39:41 -0500 Subject: [PATCH] Restore Live shelf on Today, flatten Feed to time-ordered highlights Today tab: Removed LiveSituationBar, restored the full Live game shelf below the featured Astros card where it belongs. Feed tab: Changed from two grouped shelves (condensed / highlights) to a single horizontal scroll with ALL highlights ordered by timestamp (most recent first). Added condensed game badge overlay on thumbnails. Added date field to Highlight model for time-based ordering. Co-Authored-By: Claude Opus 4.6 (1M context) --- mlbTVOS/Services/MLBServerAPI.swift | 1 + mlbTVOS/ViewModels/FeedViewModel.swift | 24 +++++++-- mlbTVOS/Views/DashboardView.swift | 22 ++------ mlbTVOS/Views/FeedView.swift | 71 +++++++++++--------------- 4 files changed, 53 insertions(+), 65 deletions(-) diff --git a/mlbTVOS/Services/MLBServerAPI.swift b/mlbTVOS/Services/MLBServerAPI.swift index 9977892..3aa7be3 100644 --- a/mlbTVOS/Services/MLBServerAPI.swift +++ b/mlbTVOS/Services/MLBServerAPI.swift @@ -305,6 +305,7 @@ actor MLBServerAPI { struct Highlight: Codable, Sendable, Identifiable { let id: String? let headline: String? + let date: String? let playbacks: [Playback]? struct Playback: Codable, Sendable { diff --git a/mlbTVOS/ViewModels/FeedViewModel.swift b/mlbTVOS/ViewModels/FeedViewModel.swift index c465b05..cfafe95 100644 --- a/mlbTVOS/ViewModels/FeedViewModel.swift +++ b/mlbTVOS/ViewModels/FeedViewModel.swift @@ -9,6 +9,15 @@ private func logFeed(_ message: String) { print("[Feed] \(message)") } +private func parseHighlightDate(_ string: String?) -> Date? { + guard let string else { return nil } + let iso = ISO8601DateFormatter() + iso.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + if let d = iso.date(from: string) { return d } + iso.formatOptions = [.withInternetDateTime] + return iso.date(from: string) +} + struct HighlightItem: Identifiable, Sendable { let id: String let headline: String @@ -18,6 +27,7 @@ struct HighlightItem: Identifiable, Sendable { let hlsURL: URL? let mp4URL: URL? let isCondensedGame: Bool + let timestamp: Date } @Observable @@ -37,7 +47,6 @@ final class FeedViewModel { let gamesWithPk = games.filter { $0.gamePk != nil } - // Fetch highlights for all games concurrently await withTaskGroup(of: [HighlightItem].self) { group in for game in gamesWithPk { group.addTask { [serverAPI] in @@ -46,7 +55,7 @@ final class FeedViewModel { gamePk: game.gamePk!, gameDate: game.gameDate ) - return raw.compactMap { highlight -> HighlightItem? in + return raw.enumerated().compactMap { index, highlight -> HighlightItem? in guard let headline = highlight.headline, let hlsStr = highlight.hlsURL ?? highlight.mp4URL, let _ = URL(string: hlsStr) else { return nil } @@ -54,15 +63,19 @@ final class FeedViewModel { let isCondensed = headline.lowercased().contains("condensed") || headline.lowercased().contains("recap") + let timestamp = parseHighlightDate(highlight.date) + ?? Date(timeIntervalSince1970: TimeInterval(index)) + return HighlightItem( - id: highlight.id ?? UUID().uuidString, + id: highlight.id ?? "\(game.gamePk!)-\(index)", headline: headline, gameTitle: "\(game.awayTeam.code) @ \(game.homeTeam.code)", awayCode: game.awayTeam.code, homeCode: game.homeTeam.code, hlsURL: highlight.hlsURL.flatMap(URL.init), mp4URL: highlight.mp4URL.flatMap(URL.init), - isCondensedGame: isCondensed + isCondensedGame: isCondensed, + timestamp: timestamp ) } } catch { @@ -75,7 +88,8 @@ final class FeedViewModel { for await batch in group { allHighlights.append(contentsOf: batch) } - highlights = allHighlights + // Sort all highlights by time, most recent first + highlights = allHighlights.sorted { $0.timestamp > $1.timestamp } } isLoading = false diff --git a/mlbTVOS/Views/DashboardView.swift b/mlbTVOS/Views/DashboardView.swift index bd1f044..5bb5b04 100644 --- a/mlbTVOS/Views/DashboardView.swift +++ b/mlbTVOS/Views/DashboardView.swift @@ -121,25 +121,6 @@ struct DashboardView: View { } .padding(.top, 80) } else { - // Live situation bar — compact strip of all live games - if !viewModel.liveGames.isEmpty { - VStack(alignment: .leading, spacing: 12) { - HStack(spacing: 8) { - LiveIndicator() - Text("LIVE NOW") - .font(DS.Fonts.caption) - .foregroundStyle(DS.Colors.live) - .kerning(1.5) - } - .padding(.horizontal, 4) - - LiveSituationBar(games: viewModel.liveGames) { game in - selectedGame = game - } - .padding(.horizontal, -horizontalPadding) - } - } - // Hero featured game if let featured = viewModel.featuredGame { FeaturedGameCard(game: featured) { @@ -147,6 +128,9 @@ struct DashboardView: View { } } + if !viewModel.liveGames.isEmpty { + gameShelf(title: "Live", icon: "antenna.radiowaves.left.and.right", games: viewModel.liveGames, excludeId: viewModel.featuredGame?.id) + } if !viewModel.scheduledGames.isEmpty { gameShelf(title: "Upcoming", icon: "calendar", games: viewModel.scheduledGames, excludeId: viewModel.featuredGame?.id) } diff --git a/mlbTVOS/Views/FeedView.swift b/mlbTVOS/Views/FeedView.swift index 66793f0..10231e3 100644 --- a/mlbTVOS/Views/FeedView.swift +++ b/mlbTVOS/Views/FeedView.swift @@ -35,23 +35,18 @@ struct FeedView: View { if viewModel.highlights.isEmpty && !viewModel.isLoading { emptyState } else { - // Condensed Games - if !viewModel.condensedGames.isEmpty { - highlightShelf( - title: "Condensed Games", - icon: "film.stack", - items: viewModel.condensedGames - ) - } - - // Latest Highlights - if !viewModel.latestHighlights.isEmpty { - highlightShelf( - title: "Latest Highlights", - icon: "play.circle.fill", - items: viewModel.latestHighlights - ) + // 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) @@ -79,31 +74,6 @@ struct FeedView: View { } } - @ViewBuilder - private func highlightShelf(title: String, icon: String, items: [HighlightItem]) -> some View { - VStack(alignment: .leading, spacing: 14) { - Label(title, systemImage: icon) - #if os(tvOS) - .font(.system(size: 24, weight: .bold, design: .rounded)) - #else - .font(.system(size: 18, weight: .bold, design: .rounded)) - #endif - .foregroundStyle(DS.Colors.textSecondary) - - ScrollView(.horizontal) { - LazyHStack(spacing: DS.Spacing.cardGap) { - ForEach(items) { item in - highlightCard(item) - .frame(width: cardWidth) - } - } - .padding(.vertical, 8) - } - .platformFocusSection() - .scrollClipDisabled() - } - } - @ViewBuilder private func highlightCard(_ item: HighlightItem) -> some View { Button { @@ -136,6 +106,25 @@ struct FeedView: View { .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))