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>
181 lines
6.9 KiB
Swift
181 lines
6.9 KiB
Swift
import SwiftUI
|
|
|
|
enum DS {
|
|
enum Colors {
|
|
static let background = Color(red: 0.03, green: 0.05, blue: 0.10)
|
|
static let backgroundElevated = Color(red: 0.06, green: 0.10, blue: 0.18)
|
|
static let navFill = Color(red: 0.07, green: 0.10, blue: 0.18).opacity(0.94)
|
|
static let panelFill = Color(red: 0.08, green: 0.11, blue: 0.18).opacity(0.94)
|
|
static let panelFillMuted = Color(red: 0.10, green: 0.14, blue: 0.23).opacity(0.84)
|
|
static let panelStroke = Color.white.opacity(0.09)
|
|
|
|
static let live = Color(red: 0.94, green: 0.25, blue: 0.28)
|
|
static let positive = Color(red: 0.24, green: 0.86, blue: 0.63)
|
|
static let warning = Color(red: 0.98, green: 0.76, blue: 0.24)
|
|
static let interactive = Color(red: 1.00, green: 0.75, blue: 0.20)
|
|
static let media = Color(red: 0.35, green: 0.78, blue: 0.95)
|
|
|
|
static let textPrimary = Color.white.opacity(0.96)
|
|
static let textSecondary = Color.white.opacity(0.76)
|
|
static let textTertiary = Color.white.opacity(0.50)
|
|
static let textQuaternary = Color.white.opacity(0.24)
|
|
|
|
static let onDarkPrimary = textPrimary
|
|
static let onDarkSecondary = textSecondary
|
|
static let onDarkTertiary = textTertiary
|
|
}
|
|
|
|
enum Shadows {
|
|
static let card = Color.black.opacity(0.30)
|
|
static let cardRadius: CGFloat = 28
|
|
static let cardY: CGFloat = 14
|
|
static let cardLifted = Color.black.opacity(0.44)
|
|
static let cardLiftedRadius: CGFloat = 42
|
|
static let cardLiftedY: CGFloat = 18
|
|
}
|
|
|
|
enum Fonts {
|
|
static let heroScore = Font.system(size: 72, weight: .black, design: .rounded).monospacedDigit()
|
|
static let largeScore = Font.system(size: 42, weight: .black, design: .rounded).monospacedDigit()
|
|
static let score = Font.system(size: 28, weight: .black, design: .rounded).monospacedDigit()
|
|
static let scoreCompact = Font.system(size: 22, weight: .bold, design: .rounded).monospacedDigit()
|
|
|
|
static let sectionTitle = Font.system(size: 30, weight: .bold, design: .rounded)
|
|
static let cardTitle = Font.system(size: 20, weight: .bold, design: .rounded)
|
|
static let cardTitleCompact = Font.system(size: 17, weight: .bold, design: .rounded)
|
|
|
|
static let dataValue = Font.system(size: 18, weight: .bold, design: .rounded).monospacedDigit()
|
|
static let dataValueCompact = Font.system(size: 15, weight: .semibold, design: .rounded).monospacedDigit()
|
|
static let body = Font.system(size: 15, weight: .medium)
|
|
static let bodySmall = Font.system(size: 13, weight: .medium)
|
|
static let caption = Font.system(size: 11, weight: .bold, design: .rounded)
|
|
|
|
#if os(tvOS)
|
|
static let tvHeroScore = Font.system(size: 94, weight: .black, design: .rounded).monospacedDigit()
|
|
static let tvSectionTitle = Font.system(size: 40, weight: .bold, design: .rounded)
|
|
static let tvCardTitle = Font.system(size: 28, weight: .bold, design: .rounded)
|
|
static let tvScore = Font.system(size: 36, weight: .black, design: .rounded).monospacedDigit()
|
|
static let tvDataValue = Font.system(size: 24, weight: .bold, design: .rounded).monospacedDigit()
|
|
static let tvBody = Font.system(size: 22, weight: .medium)
|
|
static let tvCaption = Font.system(size: 20, weight: .bold, design: .rounded)
|
|
#endif
|
|
}
|
|
|
|
enum Spacing {
|
|
#if os(tvOS)
|
|
static let panelPadCompact: CGFloat = 18
|
|
static let panelPadStandard: CGFloat = 24
|
|
static let panelPadFeatured: CGFloat = 32
|
|
static let sectionGap: CGFloat = 42
|
|
static let cardGap: CGFloat = 20
|
|
static let itemGap: CGFloat = 12
|
|
static let edgeInset: CGFloat = 50
|
|
#else
|
|
static let panelPadCompact: CGFloat = 12
|
|
static let panelPadStandard: CGFloat = 16
|
|
static let panelPadFeatured: CGFloat = 24
|
|
static let sectionGap: CGFloat = 28
|
|
static let cardGap: CGFloat = 14
|
|
static let itemGap: CGFloat = 8
|
|
static let edgeInset: CGFloat = 20
|
|
#endif
|
|
}
|
|
|
|
enum Radii {
|
|
static let compact: CGFloat = 16
|
|
static let standard: CGFloat = 24
|
|
static let featured: CGFloat = 30
|
|
}
|
|
}
|
|
|
|
struct BroadcastBackground: View {
|
|
var body: some View {
|
|
ZStack {
|
|
LinearGradient(
|
|
colors: [
|
|
Color(red: 0.03, green: 0.05, blue: 0.10),
|
|
Color(red: 0.04, green: 0.08, blue: 0.16),
|
|
Color(red: 0.02, green: 0.04, blue: 0.09),
|
|
],
|
|
startPoint: .topLeading,
|
|
endPoint: .bottomTrailing
|
|
)
|
|
|
|
// Subtle color washes — radial gradients instead of blurred circles for performance
|
|
RadialGradient(
|
|
colors: [Color(red: 0.00, green: 0.46, blue: 0.72).opacity(0.12), .clear],
|
|
center: UnitPoint(x: 0.1, y: 0.15),
|
|
startRadius: 50,
|
|
endRadius: 500
|
|
)
|
|
|
|
RadialGradient(
|
|
colors: [DS.Colors.interactive.opacity(0.10), .clear],
|
|
center: UnitPoint(x: 0.85, y: 0.15),
|
|
startRadius: 50,
|
|
endRadius: 450
|
|
)
|
|
|
|
RadialGradient(
|
|
colors: [DS.Colors.live.opacity(0.06), .clear],
|
|
center: UnitPoint(x: 0.8, y: 0.85),
|
|
startRadius: 50,
|
|
endRadius: 400
|
|
)
|
|
|
|
BroadcastGridOverlay()
|
|
.opacity(0.30)
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct BroadcastGridOverlay: View {
|
|
var body: some View {
|
|
GeometryReader { proxy in
|
|
let size = proxy.size
|
|
|
|
Path { path in
|
|
let verticalSpacing: CGFloat = 110
|
|
let horizontalSpacing: CGFloat = 90
|
|
|
|
var x: CGFloat = 0
|
|
while x <= size.width {
|
|
path.move(to: CGPoint(x: x, y: 0))
|
|
path.addLine(to: CGPoint(x: x, y: size.height))
|
|
x += verticalSpacing
|
|
}
|
|
|
|
var y: CGFloat = 0
|
|
while y <= size.height {
|
|
path.move(to: CGPoint(x: 0, y: y))
|
|
path.addLine(to: CGPoint(x: size.width, y: y))
|
|
y += horizontalSpacing
|
|
}
|
|
}
|
|
.stroke(Color.white.opacity(0.05), lineWidth: 1)
|
|
}
|
|
.allowsHitTesting(false)
|
|
}
|
|
}
|
|
|
|
struct DataLabelStyle: ViewModifier {
|
|
func body(content: Content) -> some View {
|
|
content
|
|
#if os(tvOS)
|
|
.font(DS.Fonts.tvCaption)
|
|
.kerning(1.0)
|
|
#else
|
|
.font(DS.Fonts.caption)
|
|
.kerning(1.5)
|
|
#endif
|
|
.foregroundStyle(DS.Colors.textTertiary)
|
|
.textCase(.uppercase)
|
|
}
|
|
}
|
|
|
|
extension View {
|
|
func dataLabelStyle() -> some View {
|
|
modifier(DataLabelStyle())
|
|
}
|
|
}
|