Files
MLBApp/mlbTVOS/ViewModels/FeedViewModel.swift
Trey t 39092e5f3d 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) <noreply@anthropic.com>
2026-04-12 13:39:41 -05:00

123 lines
4.2 KiB
Swift

import Foundation
import Observation
import OSLog
private let feedLogger = Logger(subsystem: "com.treyt.mlbTVOS", category: "Feed")
private func logFeed(_ message: String) {
feedLogger.debug("\(message, privacy: .public)")
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
let gameTitle: String
let awayCode: String
let homeCode: String
let hlsURL: URL?
let mp4URL: URL?
let isCondensedGame: Bool
let timestamp: Date
}
@Observable
@MainActor
final class FeedViewModel {
var highlights: [HighlightItem] = []
var isLoading = false
@ObservationIgnored
private var refreshTask: Task<Void, Never>?
private let serverAPI = MLBServerAPI()
func loadHighlights(games: [Game]) async {
isLoading = true
logFeed("loadHighlights start gameCount=\(games.count)")
let gamesWithPk = games.filter { $0.gamePk != nil }
await withTaskGroup(of: [HighlightItem].self) { group in
for game in gamesWithPk {
group.addTask { [serverAPI] in
do {
let raw = try await serverAPI.fetchHighlights(
gamePk: game.gamePk!,
gameDate: game.gameDate
)
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 }
let isCondensed = headline.lowercased().contains("condensed")
|| headline.lowercased().contains("recap")
let timestamp = parseHighlightDate(highlight.date)
?? Date(timeIntervalSince1970: TimeInterval(index))
return HighlightItem(
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,
timestamp: timestamp
)
}
} catch {
return []
}
}
}
var allHighlights: [HighlightItem] = []
for await batch in group {
allHighlights.append(contentsOf: batch)
}
// Sort all highlights by time, most recent first
highlights = allHighlights.sorted { $0.timestamp > $1.timestamp }
}
isLoading = false
logFeed("loadHighlights complete count=\(highlights.count)")
}
var condensedGames: [HighlightItem] {
highlights.filter(\.isCondensedGame)
}
var latestHighlights: [HighlightItem] {
highlights.filter { !$0.isCondensedGame }
}
func startAutoRefresh(games: [Game]) {
stopAutoRefresh()
refreshTask = Task { [weak self] in
while !Task.isCancelled {
try? await Task.sleep(for: .seconds(300))
guard !Task.isCancelled, let self else { break }
await self.loadHighlights(games: games)
}
}
}
func stopAutoRefresh() {
refreshTask?.cancel()
refreshTask = nil
}
}