Replace Feed with highlights, remove duplicate live shelf, drop Intel schedule

Feed tab: Replaced news/transaction feed with league-wide highlights and
condensed game replays. FeedViewModel now fetches highlights from all
games concurrently, splits into condensed games vs individual highlights.
Cards show team color thumbnails with play button overlay.

Today tab: Removed duplicate Live games shelf — LiveSituationBar already
shows all live games at the top, so the shelf was redundant.

Intel tab: Removed schedule section (already on Today tab). Updated
header description and stat pills. Added division hydration to standings
API call so division names display correctly instead of "Division".

Focus: Added .platformFocusable() to standings cards and leaderboard
cards so tvOS remote can scroll horizontally through them.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-04-12 13:25:36 -05:00
parent b5daddefd3
commit cd605d889d
6 changed files with 244 additions and 196 deletions

View File

@@ -9,81 +9,94 @@ private func logFeed(_ message: String) {
print("[Feed] \(message)")
}
enum FeedItemType: Sendable {
case news
case transaction
case scoring
}
struct FeedItem: Identifiable, Sendable {
struct HighlightItem: Identifiable, Sendable {
let id: String
let type: FeedItemType
let title: String
let subtitle: String
let teamCode: String?
let timestamp: Date
let headline: String
let gameTitle: String
let awayCode: String
let homeCode: String
let hlsURL: URL?
let mp4URL: URL?
let isCondensedGame: Bool
}
@Observable
@MainActor
final class FeedViewModel {
var items: [FeedItem] = []
var highlights: [HighlightItem] = []
var isLoading = false
@ObservationIgnored
private var refreshTask: Task<Void, Never>?
private let webService = MLBWebDataService()
private let serverAPI = MLBServerAPI()
func loadFeed() async {
func loadHighlights(games: [Game]) async {
isLoading = true
logFeed("loadFeed start")
logFeed("loadHighlights start gameCount=\(games.count)")
async let newsTask = webService.fetchNewsHeadlines()
async let transactionsTask = webService.fetchTransactions()
let gamesWithPk = games.filter { $0.gamePk != nil }
let news = await newsTask
let transactions = await transactionsTask
// Fetch highlights for all games concurrently
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.compactMap { highlight -> HighlightItem? in
guard let headline = highlight.headline,
let hlsStr = highlight.hlsURL ?? highlight.mp4URL,
let _ = URL(string: hlsStr) else { return nil }
var allItems: [FeedItem] = []
let isCondensed = headline.lowercased().contains("condensed")
|| headline.lowercased().contains("recap")
// News
for headline in news {
allItems.append(FeedItem(
id: "news-\(headline.id)",
type: .news,
title: headline.title,
subtitle: headline.summary,
teamCode: nil,
timestamp: headline.timestamp
))
return HighlightItem(
id: highlight.id ?? UUID().uuidString,
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
)
}
} catch {
return []
}
}
}
var allHighlights: [HighlightItem] = []
for await batch in group {
allHighlights.append(contentsOf: batch)
}
highlights = allHighlights
}
// Transactions
for tx in transactions {
allItems.append(FeedItem(
id: "tx-\(tx.id)",
type: .transaction,
title: tx.description,
subtitle: tx.type,
teamCode: tx.teamCode.isEmpty ? nil : tx.teamCode,
timestamp: tx.date
))
}
// Sort reverse chronological
items = allItems.sorted { $0.timestamp > $1.timestamp }
isLoading = false
logFeed("loadFeed complete items=\(items.count)")
logFeed("loadHighlights complete count=\(highlights.count)")
}
func startAutoRefresh() {
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.loadFeed()
await self.loadHighlights(games: games)
}
}
}