Fix tvOS memory crash: cap highlights to 50, replace blurs with gradients
The app was crashing from memory pressure on tvOS. Three causes fixed: 1. Feed was rendering all 418 highlights at once — capped to 50 items. 2. FeaturedGameCard had 3 blur effects (radius 80-120) on large circles for team color glow — replaced with a single LinearGradient. Same visual effect, fraction of the GPU memory. 3. BroadcastBackground had 3 blurred circles (radius 120-140, 680-900px) rendering on every screen — replaced with RadialGradients which are composited by the GPU natively without offscreen render passes. Also fixed iOS build: replaced tvOS-only font refs (tvSectionTitle, tvBody) with cross-platform equivalents in DashboardView fallback state. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,22 +10,21 @@ struct FeaturedGameCard: View {
|
||||
private var awayPitcherName: String? {
|
||||
game.pitchers?.components(separatedBy: " vs ").first
|
||||
}
|
||||
|
||||
private var homePitcherName: String? {
|
||||
let parts = game.pitchers?.components(separatedBy: " vs ") ?? []
|
||||
return parts.count > 1 ? parts.last : nil
|
||||
}
|
||||
|
||||
private var heroImageURL: URL? {
|
||||
// Prefer pitcher action hero photo — big, dramatic, like a cast photo
|
||||
if let pitcherId = game.homePitcherId {
|
||||
return URL(string: "https://img.mlbstatic.com/mlb-photos/image/upload/w_1200,q_auto:best/v1/people/\(pitcherId)/action/hero/current")
|
||||
return URL(string: "https://img.mlbstatic.com/mlb-photos/image/upload/w_1400,q_auto:best/v1/people/\(pitcherId)/action/hero/current")
|
||||
}
|
||||
if let pitcherId = game.awayPitcherId {
|
||||
return URL(string: "https://img.mlbstatic.com/mlb-photos/image/upload/w_1200,q_auto:best/v1/people/\(pitcherId)/action/hero/current")
|
||||
return URL(string: "https://img.mlbstatic.com/mlb-photos/image/upload/w_1400,q_auto:best/v1/people/\(pitcherId)/action/hero/current")
|
||||
}
|
||||
// Fall back to large team logo
|
||||
if let teamId = game.homeTeam.teamId {
|
||||
return URL(string: "https://midfield.mlbstatic.com/v1/team/\(teamId)/spots/800")
|
||||
return URL(string: "https://midfield.mlbstatic.com/v1/team/\(teamId)/spots/1200")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -33,116 +32,214 @@ struct FeaturedGameCard: View {
|
||||
var body: some View {
|
||||
Button(action: onSelect) {
|
||||
ZStack(alignment: .topLeading) {
|
||||
// White/cream base
|
||||
DS.Colors.panelFill
|
||||
backgroundLayer
|
||||
|
||||
// Stadium image on the right side, fading into white on the left
|
||||
HStack(spacing: 0) {
|
||||
Spacer()
|
||||
ZStack(alignment: .leading) {
|
||||
heroImage
|
||||
.frame(width: imageWidth)
|
||||
|
||||
// White fade from left edge of image
|
||||
LinearGradient(
|
||||
colors: [
|
||||
DS.Colors.panelFill,
|
||||
DS.Colors.panelFill.opacity(0.8),
|
||||
DS.Colors.panelFill.opacity(0.3),
|
||||
.clear
|
||||
],
|
||||
startPoint: .leading,
|
||||
endPoint: .trailing
|
||||
)
|
||||
.frame(width: fadeWidth)
|
||||
}
|
||||
}
|
||||
|
||||
// Text content on the left
|
||||
VStack(alignment: .leading, spacing: contentSpacing) {
|
||||
// Status badge
|
||||
statusBadge
|
||||
headerRow
|
||||
|
||||
// Giant matchup title — thin "away" bold "home"
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
HStack(spacing: 0) {
|
||||
Text(game.awayTeam.displayName)
|
||||
.font(titleThinFont)
|
||||
.foregroundStyle(DS.Colors.textSecondary)
|
||||
Text(" vs ")
|
||||
.font(titleThinFont)
|
||||
.foregroundStyle(DS.Colors.textTertiary)
|
||||
HStack(alignment: .bottom, spacing: 28) {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
scoreboardRow(team: game.awayTeam, isLeading: isWinning(away: true))
|
||||
scoreboardRow(team: game.homeTeam, isLeading: isWinning(away: false))
|
||||
}
|
||||
Text(game.homeTeam.displayName)
|
||||
.font(titleBoldFont)
|
||||
.foregroundStyle(DS.Colors.interactive)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
detailPanel
|
||||
.frame(width: detailPanelWidth, alignment: .trailing)
|
||||
}
|
||||
|
||||
// Metadata line
|
||||
metadataLine
|
||||
|
||||
// Live score or description
|
||||
if game.isLive {
|
||||
liveSection
|
||||
} else if game.isFinal {
|
||||
finalSection
|
||||
} else {
|
||||
scheduledSection
|
||||
}
|
||||
|
||||
// CTA buttons
|
||||
HStack(spacing: 14) {
|
||||
if game.hasStreams {
|
||||
Label("Watch Now", systemImage: "play.fill")
|
||||
.font(ctaFont)
|
||||
.foregroundStyle(DS.Colors.interactive)
|
||||
.padding(.horizontal, ctaPadH)
|
||||
.padding(.vertical, ctaPadV)
|
||||
.overlay(
|
||||
Capsule().strokeBorder(DS.Colors.interactive, lineWidth: 2)
|
||||
)
|
||||
}
|
||||
|
||||
Image(systemName: "plus")
|
||||
.font(ctaFont)
|
||||
.foregroundStyle(DS.Colors.textTertiary)
|
||||
.padding(ctaPadV)
|
||||
.overlay(
|
||||
Circle().strokeBorder(DS.Colors.textQuaternary, lineWidth: 1.5)
|
||||
)
|
||||
}
|
||||
insightStrip
|
||||
}
|
||||
.padding(.horizontal, heroPadH)
|
||||
.padding(.vertical, heroPadV)
|
||||
.frame(maxWidth: textAreaWidth, alignment: .topLeading)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: heroHeight)
|
||||
.clipShape(RoundedRectangle(cornerRadius: heroRadius, style: .continuous))
|
||||
.shadow(color: .black.opacity(0.08), radius: 30, y: 12)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: heroRadius, style: .continuous)
|
||||
.strokeBorder(Color.white.opacity(0.08), lineWidth: 1)
|
||||
)
|
||||
.shadow(color: .black.opacity(0.32), radius: 36, y: 18)
|
||||
}
|
||||
.platformCardStyle()
|
||||
}
|
||||
|
||||
// MARK: - Live Section
|
||||
private var backgroundLayer: some View {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: heroRadius, style: .continuous)
|
||||
.fill(
|
||||
LinearGradient(
|
||||
colors: [
|
||||
DS.Colors.panelFill,
|
||||
DS.Colors.backgroundElevated,
|
||||
Color.black.opacity(0.94),
|
||||
],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
)
|
||||
|
||||
@ViewBuilder
|
||||
private var liveSection: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 16) {
|
||||
if let away = game.awayTeam.score, let home = game.homeTeam.score {
|
||||
Text("\(away) - \(home)")
|
||||
.font(scoreFont)
|
||||
.foregroundStyle(DS.Colors.textPrimary)
|
||||
.monospacedDigit()
|
||||
.contentTransition(.numericText())
|
||||
// Team color wash — gradient instead of blur for performance
|
||||
LinearGradient(
|
||||
colors: [
|
||||
awayColor.opacity(0.2),
|
||||
.clear,
|
||||
homeColor.opacity(0.18),
|
||||
],
|
||||
startPoint: .leading,
|
||||
endPoint: .trailing
|
||||
)
|
||||
|
||||
heroImage
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.overlay {
|
||||
LinearGradient(
|
||||
colors: [
|
||||
Color.black.opacity(0.85),
|
||||
Color.black.opacity(0.48),
|
||||
Color.black.opacity(0.22),
|
||||
],
|
||||
startPoint: .leading,
|
||||
endPoint: .trailing
|
||||
)
|
||||
}
|
||||
|
||||
if let inning = game.currentInningDisplay {
|
||||
Text(inning)
|
||||
.font(inningFont)
|
||||
.foregroundStyle(DS.Colors.live)
|
||||
LinearGradient(
|
||||
colors: [
|
||||
Color.black.opacity(0.16),
|
||||
Color.black.opacity(0.52),
|
||||
],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private var headerRow: some View {
|
||||
HStack(alignment: .top, spacing: 20) {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
HStack(spacing: 10) {
|
||||
statusBadge
|
||||
|
||||
if let gameType = game.gameType, !gameType.isEmpty {
|
||||
metaBadge(gameType.uppercased(), tint: DS.Colors.media)
|
||||
}
|
||||
|
||||
if game.hasStreams {
|
||||
metaBadge("\(game.broadcasts.count) FEED\(game.broadcasts.count == 1 ? "" : "S")", tint: DS.Colors.interactive)
|
||||
}
|
||||
}
|
||||
|
||||
Text("Featured Matchup")
|
||||
.font(labelFont)
|
||||
.foregroundStyle(DS.Colors.textTertiary)
|
||||
.tracking(1.8)
|
||||
|
||||
Text(game.displayTitle)
|
||||
.font(titleFont)
|
||||
.foregroundStyle(DS.Colors.onDarkPrimary)
|
||||
.lineLimit(2)
|
||||
}
|
||||
|
||||
Spacer(minLength: 16)
|
||||
|
||||
HStack(spacing: 12) {
|
||||
if let venue = game.venue {
|
||||
summaryTag(value: venue, systemImage: "mappin.and.ellipse")
|
||||
}
|
||||
|
||||
if game.isBlackedOut {
|
||||
summaryTag(value: "Blackout", systemImage: "eye.slash.fill")
|
||||
} else if game.hasStreams {
|
||||
summaryTag(value: "Watch Now", systemImage: "play.fill")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func scoreboardRow(team: TeamInfo, isLeading: Bool) -> some View {
|
||||
HStack(spacing: 16) {
|
||||
TeamLogoView(team: team, size: logoSize)
|
||||
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
Text(team.code)
|
||||
.font(codeFont)
|
||||
.foregroundStyle(.white)
|
||||
|
||||
Text(team.displayName)
|
||||
.font(nameFont)
|
||||
.foregroundStyle(DS.Colors.onDarkSecondary)
|
||||
|
||||
HStack(spacing: 10) {
|
||||
if let record = team.record {
|
||||
Text(record)
|
||||
.font(metadataFont)
|
||||
.foregroundStyle(DS.Colors.onDarkSecondary)
|
||||
}
|
||||
|
||||
if let summary = team.standingSummary {
|
||||
Text(summary)
|
||||
.font(metadataFont)
|
||||
.foregroundStyle(DS.Colors.onDarkTertiary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(minLength: 8)
|
||||
|
||||
Text(team.score.map(String.init) ?? "—")
|
||||
.font(scoreFont)
|
||||
.foregroundStyle(isLeading ? .white : DS.Colors.onDarkSecondary)
|
||||
.monospacedDigit()
|
||||
}
|
||||
.padding(.horizontal, rowPadH)
|
||||
.padding(.vertical, rowPadV)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 22, style: .continuous)
|
||||
.fill(Color.black.opacity(isLeading ? 0.34 : 0.22))
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: 22, style: .continuous)
|
||||
.strokeBorder(Color.white.opacity(isLeading ? 0.10 : 0.06), lineWidth: 1)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var detailPanel: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
switch game.status {
|
||||
case .live:
|
||||
livePanel
|
||||
case .final_:
|
||||
finalPanel
|
||||
case .scheduled:
|
||||
scheduledPanel
|
||||
case .unknown:
|
||||
statusFallbackPanel
|
||||
}
|
||||
}
|
||||
.padding(detailPanelPad)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 26, style: .continuous)
|
||||
.fill(Color.black.opacity(0.34))
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: 26, style: .continuous)
|
||||
.strokeBorder(Color.white.opacity(0.08), lineWidth: 1)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private var livePanel: some View {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
Text("Live Situation")
|
||||
.font(panelLabelFont)
|
||||
.foregroundStyle(DS.Colors.textTertiary)
|
||||
|
||||
Text(game.currentInningDisplay ?? "Live")
|
||||
.font(panelValueFont)
|
||||
.foregroundStyle(.white)
|
||||
|
||||
if let linescore = game.linescore {
|
||||
DiamondView(
|
||||
@@ -150,93 +247,172 @@ struct FeaturedGameCard: View {
|
||||
strikes: linescore.strikes ?? 0,
|
||||
outs: linescore.outs ?? 0
|
||||
)
|
||||
|
||||
if let awayRuns = linescore.teams?.away?.runs,
|
||||
let homeRuns = linescore.teams?.home?.runs,
|
||||
let awayHits = linescore.teams?.away?.hits,
|
||||
let homeHits = linescore.teams?.home?.hits {
|
||||
HStack(spacing: 14) {
|
||||
detailMetric(label: game.awayTeam.code, value: "\(awayRuns)R / \(awayHits)H")
|
||||
detailMetric(label: game.homeTeam.code, value: "\(homeRuns)R / \(homeHits)H")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Final Section
|
||||
private var finalPanel: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("Final")
|
||||
.font(panelLabelFont)
|
||||
.foregroundStyle(DS.Colors.textTertiary)
|
||||
|
||||
@ViewBuilder
|
||||
private var finalSection: some View {
|
||||
if let away = game.awayTeam.score, let home = game.homeTeam.score {
|
||||
HStack(spacing: 12) {
|
||||
Text("\(away) - \(home)")
|
||||
.font(scoreFont)
|
||||
.foregroundStyle(DS.Colors.textPrimary)
|
||||
.monospacedDigit()
|
||||
Text("FINAL")
|
||||
.font(inningFont)
|
||||
.foregroundStyle(DS.Colors.textTertiary)
|
||||
Text(game.scoreDisplay ?? "Complete")
|
||||
.font(panelValueFont)
|
||||
.foregroundStyle(.white)
|
||||
|
||||
Text("Box score, play timeline, and highlights are ready in Game Center.")
|
||||
.font(panelBodyFont)
|
||||
.foregroundStyle(DS.Colors.onDarkSecondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
|
||||
private var scheduledPanel: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("Starting Pitchers")
|
||||
.font(panelLabelFont)
|
||||
.foregroundStyle(DS.Colors.textTertiary)
|
||||
|
||||
Text(pitcherMatchupText)
|
||||
.font(panelValueFont)
|
||||
.foregroundStyle(.white)
|
||||
.lineLimit(3)
|
||||
|
||||
if let startTime = game.startTime {
|
||||
Text("First pitch \(startTime)")
|
||||
.font(panelBodyFont)
|
||||
.foregroundStyle(DS.Colors.onDarkSecondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Scheduled Section
|
||||
private var statusFallbackPanel: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("Game State")
|
||||
.font(panelLabelFont)
|
||||
.foregroundStyle(DS.Colors.textTertiary)
|
||||
|
||||
@ViewBuilder
|
||||
private var scheduledSection: some View {
|
||||
Text(game.status.label.isEmpty ? "Awaiting update" : game.status.label)
|
||||
.font(panelValueFont)
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
}
|
||||
|
||||
private var insightStrip: some View {
|
||||
HStack(spacing: 14) {
|
||||
insightCard(
|
||||
title: "Pitching",
|
||||
value: pitcherInsightText,
|
||||
accent: DS.Colors.media
|
||||
)
|
||||
|
||||
insightCard(
|
||||
title: "Venue",
|
||||
value: game.venue ?? "TBD",
|
||||
accent: DS.Colors.interactive
|
||||
)
|
||||
|
||||
insightCard(
|
||||
title: "Feeds",
|
||||
value: game.isBlackedOut ? "Blackout" : "\(game.broadcasts.count) available",
|
||||
accent: game.isBlackedOut ? DS.Colors.live : DS.Colors.positive
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func insightCard(title: String, value: String, accent: Color) -> some View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
if let pitchers = game.pitchers {
|
||||
Text(pitchers)
|
||||
.font(descFont)
|
||||
.foregroundStyle(DS.Colors.textSecondary)
|
||||
.lineLimit(2)
|
||||
}
|
||||
Text(title)
|
||||
.font(insightTitleFont)
|
||||
.foregroundStyle(DS.Colors.textTertiary)
|
||||
|
||||
Text(value)
|
||||
.font(insightValueFont)
|
||||
.foregroundStyle(.white)
|
||||
.lineLimit(2)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 14)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 20, style: .continuous)
|
||||
.fill(accent.opacity(0.14))
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: 20, style: .continuous)
|
||||
.strokeBorder(accent.opacity(0.20), lineWidth: 1)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private func detailMetric(label: String, value: String) -> some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(label)
|
||||
.font(insightTitleFont)
|
||||
.foregroundStyle(DS.Colors.textTertiary)
|
||||
|
||||
Text(value)
|
||||
.font(insightValueFont)
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Status Badge
|
||||
private func summaryTag(value: String, systemImage: String) -> some View {
|
||||
Label(value, systemImage: systemImage)
|
||||
.font(summaryFont)
|
||||
.foregroundStyle(.white.opacity(0.92))
|
||||
.padding(.horizontal, 14)
|
||||
.padding(.vertical, 10)
|
||||
.background(
|
||||
Capsule()
|
||||
.fill(Color.black.opacity(0.28))
|
||||
.overlay {
|
||||
Capsule()
|
||||
.strokeBorder(Color.white.opacity(0.08), lineWidth: 1)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private func metaBadge(_ value: String, tint: Color) -> some View {
|
||||
Text(value)
|
||||
.font(badgeFont)
|
||||
.foregroundStyle(tint)
|
||||
.padding(.horizontal, 14)
|
||||
.padding(.vertical, 9)
|
||||
.background(
|
||||
Capsule()
|
||||
.fill(tint.opacity(0.14))
|
||||
.overlay {
|
||||
Capsule()
|
||||
.strokeBorder(tint.opacity(0.22), lineWidth: 1)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var statusBadge: some View {
|
||||
switch game.status {
|
||||
case .live(let inning):
|
||||
HStack(spacing: 6) {
|
||||
Circle().fill(DS.Colors.live).frame(width: 8, height: 8)
|
||||
Text(inning ?? "LIVE")
|
||||
.font(badgeFont)
|
||||
.foregroundStyle(DS.Colors.live)
|
||||
}
|
||||
|
||||
metaBadge(inning?.uppercased() ?? "LIVE", tint: DS.Colors.live)
|
||||
case .scheduled(let time):
|
||||
Text(time)
|
||||
.font(badgeFont)
|
||||
.foregroundStyle(DS.Colors.textTertiary)
|
||||
|
||||
metaBadge(time.uppercased(), tint: DS.Colors.warning)
|
||||
case .final_:
|
||||
Text("FINAL")
|
||||
.font(badgeFont)
|
||||
.foregroundStyle(DS.Colors.textTertiary)
|
||||
|
||||
metaBadge("FINAL", tint: DS.Colors.positive)
|
||||
case .unknown:
|
||||
EmptyView()
|
||||
metaBadge("PENDING", tint: DS.Colors.textTertiary)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Metadata Line
|
||||
|
||||
@ViewBuilder
|
||||
private var metadataLine: some View {
|
||||
HStack(spacing: metaSeparatorWidth) {
|
||||
if let venue = game.venue {
|
||||
Text(venue)
|
||||
}
|
||||
if let record = game.awayTeam.record {
|
||||
Text("\(game.awayTeam.code) \(record)")
|
||||
}
|
||||
if let record = game.homeTeam.record {
|
||||
Text("\(game.homeTeam.code) \(record)")
|
||||
}
|
||||
if !game.broadcasts.isEmpty {
|
||||
Text("\(game.broadcasts.count) feed\(game.broadcasts.count == 1 ? "" : "s")")
|
||||
}
|
||||
}
|
||||
.font(metaFont)
|
||||
.foregroundStyle(DS.Colors.textTertiary)
|
||||
}
|
||||
|
||||
// MARK: - Stadium Image
|
||||
|
||||
@ViewBuilder
|
||||
private var heroImage: some View {
|
||||
if let url = heroImageURL {
|
||||
@@ -246,8 +422,6 @@ struct FeaturedGameCard: View {
|
||||
image
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(height: heroHeight)
|
||||
.clipped()
|
||||
default:
|
||||
fallbackImage
|
||||
}
|
||||
@@ -257,79 +431,88 @@ struct FeaturedGameCard: View {
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var fallbackImage: some View {
|
||||
ZStack {
|
||||
// Rich team color gradient
|
||||
LinearGradient(
|
||||
colors: [
|
||||
awayColor.opacity(0.4),
|
||||
homeColor.opacity(0.6),
|
||||
],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
|
||||
// Large prominent team logos
|
||||
HStack(spacing: fallbackLogoGap) {
|
||||
TeamLogoView(team: game.awayTeam, size: fallbackLogoSize)
|
||||
.shadow(color: .black.opacity(0.2), radius: 12, y: 4)
|
||||
Text("vs")
|
||||
.font(.system(size: fallbackLogoSize * 0.3, weight: .light))
|
||||
.foregroundStyle(.white.opacity(0.5))
|
||||
TeamLogoView(team: game.homeTeam, size: fallbackLogoSize)
|
||||
.shadow(color: .black.opacity(0.2), radius: 12, y: 4)
|
||||
}
|
||||
}
|
||||
LinearGradient(
|
||||
colors: [
|
||||
awayColor.opacity(0.32),
|
||||
homeColor.opacity(0.28),
|
||||
Color.clear,
|
||||
],
|
||||
startPoint: .leading,
|
||||
endPoint: .trailing
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Platform Sizing
|
||||
private var pitcherMatchupText: String {
|
||||
if let awayPitcherName, let homePitcherName {
|
||||
return "\(awayPitcherName)\nvs \(homePitcherName)"
|
||||
}
|
||||
return game.pitchers ?? "Pitchers pending"
|
||||
}
|
||||
|
||||
private var pitcherInsightText: String {
|
||||
if let awayPitcherName, let homePitcherName {
|
||||
return "\(awayPitcherName) vs \(homePitcherName)"
|
||||
}
|
||||
return game.pitchers ?? "Awaiting starters"
|
||||
}
|
||||
|
||||
private func isWinning(away: Bool) -> Bool {
|
||||
guard let awayScore = game.awayTeam.score, let homeScore = game.homeTeam.score else {
|
||||
return false
|
||||
}
|
||||
return away ? awayScore > homeScore : homeScore > awayScore
|
||||
}
|
||||
|
||||
#if os(tvOS)
|
||||
private var heroHeight: CGFloat { 480 }
|
||||
private var heroRadius: CGFloat { 28 }
|
||||
private var heroPadH: CGFloat { 60 }
|
||||
private var heroPadV: CGFloat { 50 }
|
||||
private var contentSpacing: CGFloat { 16 }
|
||||
private var imageWidth: CGFloat { 900 }
|
||||
private var fadeWidth: CGFloat { 400 }
|
||||
private var textAreaWidth: CGFloat { 700 }
|
||||
private var metaSeparatorWidth: CGFloat { 18 }
|
||||
private var fallbackLogoSize: CGFloat { 120 }
|
||||
private var fallbackLogoGap: CGFloat { 40 }
|
||||
|
||||
private var titleThinFont: Font { .system(size: 48, weight: .light) }
|
||||
private var titleBoldFont: Font { .system(size: 52, weight: .black, design: .rounded) }
|
||||
private var scoreFont: Font { .system(size: 64, weight: .black, design: .rounded) }
|
||||
private var inningFont: Font { .system(size: 28, weight: .bold, design: .rounded) }
|
||||
private var badgeFont: Font { .system(size: 22, weight: .bold, design: .rounded) }
|
||||
private var metaFont: Font { .system(size: 22, weight: .medium) }
|
||||
private var descFont: Font { .system(size: 24, weight: .medium) }
|
||||
private var ctaFont: Font { .system(size: 24, weight: .bold) }
|
||||
private var ctaPadH: CGFloat { 32 }
|
||||
private var ctaPadV: CGFloat { 14 }
|
||||
private var heroHeight: CGFloat { 470 }
|
||||
private var heroRadius: CGFloat { 34 }
|
||||
private var heroPadH: CGFloat { 36 }
|
||||
private var heroPadV: CGFloat { 34 }
|
||||
private var detailPanelWidth: CGFloat { 360 }
|
||||
private var detailPanelPad: CGFloat { 26 }
|
||||
private var detailPanelWidthCompact: CGFloat { 320 }
|
||||
private var contentSpacing: CGFloat { 26 }
|
||||
private var logoSize: CGFloat { 56 }
|
||||
private var rowPadH: CGFloat { 22 }
|
||||
private var rowPadV: CGFloat { 18 }
|
||||
private var titleFont: Font { .system(size: 52, weight: .black, design: .rounded) }
|
||||
private var labelFont: Font { .system(size: 15, weight: .black, design: .rounded) }
|
||||
private var codeFont: Font { .system(size: 22, weight: .black, design: .rounded) }
|
||||
private var nameFont: Font { .system(size: 28, weight: .bold, design: .rounded) }
|
||||
private var metadataFont: Font { .system(size: 18, weight: .bold, design: .rounded) }
|
||||
private var scoreFont: Font { .system(size: 60, weight: .black, design: .rounded).monospacedDigit() }
|
||||
private var badgeFont: Font { .system(size: 13, weight: .black, design: .rounded) }
|
||||
private var summaryFont: Font { .system(size: 16, weight: .bold, design: .rounded) }
|
||||
private var panelLabelFont: Font { .system(size: 14, weight: .black, design: .rounded) }
|
||||
private var panelValueFont: Font { .system(size: 28, weight: .black, design: .rounded) }
|
||||
private var panelBodyFont: Font { .system(size: 18, weight: .semibold) }
|
||||
private var insightTitleFont: Font { .system(size: 13, weight: .black, design: .rounded) }
|
||||
private var insightValueFont: Font { .system(size: 18, weight: .bold, design: .rounded) }
|
||||
#else
|
||||
private var heroHeight: CGFloat { 340 }
|
||||
private var heroRadius: CGFloat { 22 }
|
||||
private var heroPadH: CGFloat { 28 }
|
||||
private var heroPadV: CGFloat { 28 }
|
||||
private var contentSpacing: CGFloat { 10 }
|
||||
private var imageWidth: CGFloat { 400 }
|
||||
private var fadeWidth: CGFloat { 200 }
|
||||
private var textAreaWidth: CGFloat { 350 }
|
||||
private var metaSeparatorWidth: CGFloat { 12 }
|
||||
private var fallbackLogoSize: CGFloat { 60 }
|
||||
private var fallbackLogoGap: CGFloat { 20 }
|
||||
|
||||
private var titleThinFont: Font { .system(size: 28, weight: .light) }
|
||||
private var titleBoldFont: Font { .system(size: 32, weight: .black, design: .rounded) }
|
||||
private var scoreFont: Font { .system(size: 40, weight: .black, design: .rounded) }
|
||||
private var inningFont: Font { .system(size: 18, weight: .bold, design: .rounded) }
|
||||
private var badgeFont: Font { .system(size: 14, weight: .bold, design: .rounded) }
|
||||
private var metaFont: Font { .system(size: 14, weight: .medium) }
|
||||
private var descFont: Font { .system(size: 15, weight: .medium) }
|
||||
private var ctaFont: Font { .system(size: 16, weight: .bold) }
|
||||
private var ctaPadH: CGFloat { 22 }
|
||||
private var ctaPadV: CGFloat { 10 }
|
||||
private var heroRadius: CGFloat { 26 }
|
||||
private var heroPadH: CGFloat { 22 }
|
||||
private var heroPadV: CGFloat { 22 }
|
||||
private var detailPanelWidth: CGFloat { 250 }
|
||||
private var detailPanelPad: CGFloat { 18 }
|
||||
private var detailPanelWidthCompact: CGFloat { 240 }
|
||||
private var contentSpacing: CGFloat { 18 }
|
||||
private var logoSize: CGFloat { 36 }
|
||||
private var rowPadH: CGFloat { 14 }
|
||||
private var rowPadV: CGFloat { 12 }
|
||||
private var titleFont: Font { .system(size: 30, weight: .black, design: .rounded) }
|
||||
private var labelFont: Font { .system(size: 11, weight: .black, design: .rounded) }
|
||||
private var codeFont: Font { .system(size: 15, weight: .black, design: .rounded) }
|
||||
private var nameFont: Font { .system(size: 18, weight: .bold, design: .rounded) }
|
||||
private var metadataFont: Font { .system(size: 12, weight: .bold, design: .rounded) }
|
||||
private var scoreFont: Font { .system(size: 32, weight: .black, design: .rounded).monospacedDigit() }
|
||||
private var badgeFont: Font { .system(size: 10, weight: .black, design: .rounded) }
|
||||
private var summaryFont: Font { .system(size: 11, weight: .bold, design: .rounded) }
|
||||
private var panelLabelFont: Font { .system(size: 10, weight: .black, design: .rounded) }
|
||||
private var panelValueFont: Font { .system(size: 18, weight: .black, design: .rounded) }
|
||||
private var panelBodyFont: Font { .system(size: 13, weight: .semibold) }
|
||||
private var insightTitleFont: Font { .system(size: 10, weight: .black, design: .rounded) }
|
||||
private var insightValueFont: Font { .system(size: 12, weight: .bold, design: .rounded) }
|
||||
#endif
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user