Add UI redesign: design system, dashboard density, game intelligence, feed tab, league leaders
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>
This commit is contained in:
236
mlbTVOS/Services/MLBWebDataService.swift
Normal file
236
mlbTVOS/Services/MLBWebDataService.swift
Normal file
@@ -0,0 +1,236 @@
|
||||
import Foundation
|
||||
import OSLog
|
||||
|
||||
private let webDataLogger = Logger(subsystem: "com.treyt.mlbTVOS", category: "WebData")
|
||||
|
||||
private func logWebData(_ message: String) {
|
||||
webDataLogger.debug("\(message, privacy: .public)")
|
||||
print("[WebData] \(message)")
|
||||
}
|
||||
|
||||
// MARK: - Models
|
||||
|
||||
struct NewsHeadline: Identifiable, Sendable {
|
||||
let id: String
|
||||
let title: String
|
||||
let summary: String
|
||||
let timestamp: Date
|
||||
let imageURL: URL?
|
||||
}
|
||||
|
||||
struct InjuryReport: Identifiable, Sendable {
|
||||
let id: String
|
||||
let teamCode: String
|
||||
let playerName: String
|
||||
let position: String
|
||||
let status: String // "10-Day IL", "60-Day IL", "Day-to-Day"
|
||||
let date: Date
|
||||
let notes: String
|
||||
}
|
||||
|
||||
struct Transaction: Identifiable, Sendable {
|
||||
let id: String
|
||||
let teamCode: String
|
||||
let description: String
|
||||
let date: Date
|
||||
let type: String // "Trade", "DFA", "Call-Up", "Placed on IL"
|
||||
}
|
||||
|
||||
struct LeagueLeader: Identifiable, Sendable {
|
||||
let id: String
|
||||
let rank: Int
|
||||
let playerName: String
|
||||
let teamCode: String
|
||||
let value: String
|
||||
let personId: Int?
|
||||
}
|
||||
|
||||
struct LeaderCategory: Identifiable, Sendable {
|
||||
let id: String
|
||||
let name: String
|
||||
let leaders: [LeagueLeader]
|
||||
}
|
||||
|
||||
// MARK: - Service
|
||||
|
||||
actor MLBWebDataService {
|
||||
private let statsAPI = MLBStatsAPI()
|
||||
private let session: URLSession
|
||||
|
||||
init() {
|
||||
let config = URLSessionConfiguration.default
|
||||
config.timeoutIntervalForRequest = 15
|
||||
session = URLSession(configuration: config)
|
||||
}
|
||||
|
||||
// MARK: - League Leaders (via Stats API)
|
||||
|
||||
func fetchLeagueLeaders() async throws -> [LeaderCategory] {
|
||||
let categories = [
|
||||
("homeRuns", "Home Runs"),
|
||||
("battingAverage", "Batting Average"),
|
||||
("runsBattedIn", "RBI"),
|
||||
("earnedRunAverage", "ERA"),
|
||||
("strikeouts", "Strikeouts"),
|
||||
("wins", "Wins"),
|
||||
("saves", "Saves"),
|
||||
("stolenBases", "Stolen Bases"),
|
||||
("onBasePlusSlugging", "OPS"),
|
||||
("walksHitsPerInningPitched", "WHIP"),
|
||||
]
|
||||
|
||||
var results: [LeaderCategory] = []
|
||||
|
||||
for (code, name) in categories {
|
||||
let url = URL(string: "https://statsapi.mlb.com/api/v1/stats/leaders?leaderCategories=\(code)&season=\(currentSeason)&limit=5&sportId=1")!
|
||||
logWebData("fetchLeagueLeaders category=\(code)")
|
||||
|
||||
do {
|
||||
let (data, _) = try await session.data(from: url)
|
||||
let response = try JSONDecoder().decode(LeaderResponse.self, from: data)
|
||||
|
||||
if let category = response.leagueLeaders?.first {
|
||||
let leaders = (category.leaders ?? []).prefix(5).enumerated().map { index, leader in
|
||||
LeagueLeader(
|
||||
id: "\(code)-\(index)",
|
||||
rank: leader.rank ?? (index + 1),
|
||||
playerName: leader.person?.fullName ?? "Unknown",
|
||||
teamCode: leader.team?.abbreviation ?? "",
|
||||
value: leader.value ?? "",
|
||||
personId: leader.person?.id
|
||||
)
|
||||
}
|
||||
results.append(LeaderCategory(id: code, name: name, leaders: Array(leaders)))
|
||||
}
|
||||
} catch {
|
||||
logWebData("fetchLeagueLeaders FAILED category=\(code) error=\(error)")
|
||||
}
|
||||
}
|
||||
|
||||
logWebData("fetchLeagueLeaders complete categories=\(results.count)")
|
||||
return results
|
||||
}
|
||||
|
||||
// MARK: - News Headlines (from MLB.com)
|
||||
|
||||
func fetchNewsHeadlines() async -> [NewsHeadline] {
|
||||
let url = URL(string: "https://statsapi.mlb.com/api/v1/news?sportId=1")!
|
||||
logWebData("fetchNewsHeadlines")
|
||||
|
||||
do {
|
||||
let (data, _) = try await session.data(from: url)
|
||||
let response = try JSONDecoder().decode(NewsResponse.self, from: data)
|
||||
|
||||
let headlines = (response.articles ?? []).prefix(15).map { article in
|
||||
NewsHeadline(
|
||||
id: article.slug ?? UUID().uuidString,
|
||||
title: article.headline ?? "Untitled",
|
||||
summary: article.subhead ?? "",
|
||||
timestamp: ISO8601DateFormatter().date(from: article.date ?? "") ?? Date(),
|
||||
imageURL: article.image?.cuts?.first?.src.flatMap(URL.init)
|
||||
)
|
||||
}
|
||||
logWebData("fetchNewsHeadlines success count=\(headlines.count)")
|
||||
return Array(headlines)
|
||||
} catch {
|
||||
logWebData("fetchNewsHeadlines FAILED error=\(error)")
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Transactions (from Stats API)
|
||||
|
||||
func fetchTransactions() async -> [Transaction] {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "MM/dd/yyyy"
|
||||
let today = formatter.string(from: Date())
|
||||
let weekAgo = formatter.string(from: Calendar.current.date(byAdding: .day, value: -7, to: Date())!)
|
||||
|
||||
let url = URL(string: "https://statsapi.mlb.com/api/v1/transactions?startDate=\(weekAgo)&endDate=\(today)&sportId=1")!
|
||||
logWebData("fetchTransactions from=\(weekAgo) to=\(today)")
|
||||
|
||||
do {
|
||||
let (data, _) = try await session.data(from: url)
|
||||
let response = try JSONDecoder().decode(TransactionResponse.self, from: data)
|
||||
|
||||
let transactions = (response.transactions ?? []).prefix(30).enumerated().map { index, tx in
|
||||
Transaction(
|
||||
id: "\(tx.id ?? index)",
|
||||
teamCode: tx.team?.abbreviation ?? "",
|
||||
description: tx.description ?? "",
|
||||
date: ISO8601DateFormatter().date(from: tx.date ?? "") ?? Date(),
|
||||
type: tx.typeDesc ?? "Transaction"
|
||||
)
|
||||
}
|
||||
logWebData("fetchTransactions success count=\(transactions.count)")
|
||||
return Array(transactions)
|
||||
} catch {
|
||||
logWebData("fetchTransactions FAILED error=\(error)")
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
private var currentSeason: Int {
|
||||
Calendar.current.component(.year, from: Date())
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - API Response Models
|
||||
|
||||
private struct LeaderResponse: Codable {
|
||||
let leagueLeaders: [LeaderCategoryResponse]?
|
||||
}
|
||||
|
||||
private struct LeaderCategoryResponse: Codable {
|
||||
let leaderCategory: String?
|
||||
let leaders: [LeaderEntry]?
|
||||
}
|
||||
|
||||
private struct LeaderEntry: Codable {
|
||||
let rank: Int?
|
||||
let value: String?
|
||||
let person: PersonRef?
|
||||
let team: TeamRef?
|
||||
}
|
||||
|
||||
private struct PersonRef: Codable {
|
||||
let id: Int?
|
||||
let fullName: String?
|
||||
}
|
||||
|
||||
private struct TeamRef: Codable {
|
||||
let id: Int?
|
||||
let abbreviation: String?
|
||||
}
|
||||
|
||||
private struct NewsResponse: Codable {
|
||||
let articles: [NewsArticle]?
|
||||
}
|
||||
|
||||
private struct NewsArticle: Codable {
|
||||
let slug: String?
|
||||
let headline: String?
|
||||
let subhead: String?
|
||||
let date: String?
|
||||
let image: NewsImage?
|
||||
}
|
||||
|
||||
private struct NewsImage: Codable {
|
||||
let cuts: [NewsImageCut]?
|
||||
}
|
||||
|
||||
private struct NewsImageCut: Codable {
|
||||
let src: String?
|
||||
}
|
||||
|
||||
private struct TransactionResponse: Codable {
|
||||
let transactions: [TransactionEntry]?
|
||||
}
|
||||
|
||||
private struct TransactionEntry: Codable {
|
||||
let id: Int?
|
||||
let description: String?
|
||||
let date: String?
|
||||
let typeDesc: String?
|
||||
let team: TeamRef?
|
||||
}
|
||||
Reference in New Issue
Block a user