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>
123 lines
4.2 KiB
Swift
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
|
|
}
|
|
}
|