Phase 1 - Design System: DesignSystem.swift (typography, colors, spacing constants) and DataPanel.swift (reusable panel container with 3 densities and optional team accent bar). Phase 2 - Dashboard Density: LiveSituationBar (compact strip of all live games with scores/innings/outs), MiniLinescoreView (R-H-E footer for game cards), DiamondView (visual baseball diamond with runners and count). Dashboard shows live situation bar when games are active. Game cards now display mini linescore for live/final games. Phase 3 - Game Center Intelligence: WinProbabilityChartView (full-game line chart using Swift Charts with area fills), PitchArsenalView (pitch type distribution with velocity bars). GameCenterViewModel now stores full WP history array instead of just latest values. Phase 4 - Feed Tab: MLBWebDataService (fetches league leaders from Stats API, news headlines, transactions), FeedViewModel, FeedView with reverse-chronological feed items. FeedItemView with colored edge bars by category. Added 5th "Feed" tab to both tvOS and iOS. Phase 5 - Intel Tab: LeaderboardView (top-5 stat cards with headshots), integrated into LeagueCenterView. Renamed tabs: Games->Today, League->Intel. LeagueCenterViewModel now fetches league leaders. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
149 lines
4.2 KiB
Swift
149 lines
4.2 KiB
Swift
import Foundation
|
|
import Observation
|
|
|
|
@Observable
|
|
@MainActor
|
|
final class LeagueCenterViewModel {
|
|
var scheduleGames: [StatsGame] = []
|
|
var standings: [StandingsDivisionRecord] = []
|
|
var teams: [LeagueTeamSummary] = []
|
|
var leagueLeaders: [LeaderCategory] = []
|
|
var isLoadingLeaders = false
|
|
|
|
var selectedTeam: TeamProfile?
|
|
var roster: [RosterPlayerSummary] = []
|
|
var selectedPlayer: PlayerProfile?
|
|
|
|
var isLoadingOverview = false
|
|
var isLoadingTeam = false
|
|
var isLoadingPlayer = false
|
|
|
|
var overviewErrorMessage: String?
|
|
var teamErrorMessage: String?
|
|
var playerErrorMessage: String?
|
|
|
|
private let statsAPI = MLBStatsAPI()
|
|
private let webService = MLBWebDataService()
|
|
private(set) var scheduleDate = Date()
|
|
|
|
private var seasonString: String {
|
|
String(Calendar.current.component(.year, from: scheduleDate))
|
|
}
|
|
|
|
private var scheduleDateString: String {
|
|
Self.scheduleFormatter.string(from: scheduleDate)
|
|
}
|
|
|
|
var displayDateString: String {
|
|
Self.displayFormatter.string(from: scheduleDate)
|
|
}
|
|
|
|
func loadInitial() async {
|
|
isLoadingOverview = true
|
|
overviewErrorMessage = nil
|
|
|
|
async let scheduleTask = statsAPI.fetchSchedule(date: scheduleDateString)
|
|
async let standingsTask = statsAPI.fetchStandingsRecords(season: seasonString)
|
|
async let teamsTask = statsAPI.fetchLeagueTeams(season: seasonString)
|
|
|
|
do {
|
|
scheduleGames = try await scheduleTask
|
|
standings = try await standingsTask
|
|
teams = try await teamsTask
|
|
|
|
if selectedTeam == nil, let firstTeam = teams.first {
|
|
await selectTeam(firstTeam.id)
|
|
}
|
|
} catch {
|
|
overviewErrorMessage = "Failed to load league information."
|
|
}
|
|
|
|
isLoadingOverview = false
|
|
|
|
// Load leaders in the background
|
|
Task { await loadLeaders() }
|
|
}
|
|
|
|
func loadLeaders() async {
|
|
isLoadingLeaders = true
|
|
do {
|
|
leagueLeaders = try await webService.fetchLeagueLeaders()
|
|
} catch {
|
|
// Leaders are supplementary
|
|
}
|
|
isLoadingLeaders = false
|
|
}
|
|
|
|
func goToPreviousDay() async {
|
|
scheduleDate = Calendar.current.date(byAdding: .day, value: -1, to: scheduleDate) ?? scheduleDate
|
|
await loadSchedule()
|
|
}
|
|
|
|
func goToNextDay() async {
|
|
scheduleDate = Calendar.current.date(byAdding: .day, value: 1, to: scheduleDate) ?? scheduleDate
|
|
await loadSchedule()
|
|
}
|
|
|
|
func goToToday() async {
|
|
scheduleDate = Date()
|
|
await loadSchedule()
|
|
}
|
|
|
|
func loadSchedule() async {
|
|
do {
|
|
scheduleGames = try await statsAPI.fetchSchedule(date: scheduleDateString)
|
|
} catch {
|
|
overviewErrorMessage = "Failed to load schedule."
|
|
}
|
|
}
|
|
|
|
func selectTeam(_ teamID: Int) async {
|
|
isLoadingTeam = true
|
|
teamErrorMessage = nil
|
|
selectedPlayer = nil
|
|
playerErrorMessage = nil
|
|
|
|
do {
|
|
async let profileTask = statsAPI.fetchTeamProfile(teamID: teamID, season: seasonString)
|
|
async let rosterTask = statsAPI.fetchTeamRoster(teamID: teamID, season: seasonString)
|
|
|
|
selectedTeam = try await profileTask
|
|
roster = try await rosterTask
|
|
|
|
if let firstPlayer = roster.first {
|
|
await selectPlayer(firstPlayer.id)
|
|
}
|
|
} catch {
|
|
teamErrorMessage = "Failed to load team details."
|
|
roster = []
|
|
}
|
|
|
|
isLoadingTeam = false
|
|
}
|
|
|
|
func selectPlayer(_ playerID: Int) async {
|
|
isLoadingPlayer = true
|
|
playerErrorMessage = nil
|
|
|
|
do {
|
|
selectedPlayer = try await statsAPI.fetchPlayerProfile(personID: playerID, season: seasonString)
|
|
} catch {
|
|
playerErrorMessage = "Failed to load player details."
|
|
}
|
|
|
|
isLoadingPlayer = false
|
|
}
|
|
|
|
private static let scheduleFormatter: DateFormatter = {
|
|
let formatter = DateFormatter()
|
|
formatter.dateFormat = "yyyy-MM-dd"
|
|
return formatter
|
|
}()
|
|
|
|
private static let displayFormatter: DateFormatter = {
|
|
let formatter = DateFormatter()
|
|
formatter.dateFormat = "EEEE, MMM d"
|
|
return formatter
|
|
}()
|
|
}
|