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:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user