import SwiftUI struct SettingsView: View { @Environment(GamesViewModel.self) private var viewModel private let resolutions = [ ("best", "Best Quality"), ("1080p60", "1080p 60fps"), ("720p60", "720p 60fps"), ("540p", "540p"), ("adaptive", "Adaptive"), ] var body: some View { @Bindable var vm = viewModel ScrollView { VStack(alignment: .leading, spacing: 26) { header settingsPanel( title: "Server", subtitle: "Current upstream endpoint and playback defaults." ) { infoRow(label: "Base URL", value: viewModel.serverBaseURL) infoRow(label: "Current Quality", value: resolutionLabel(for: viewModel.defaultResolution)) } settingsPanel( title: "Playback Quality", subtitle: "Preferred stream profile for newly opened feeds." ) { VStack(spacing: 12) { ForEach(resolutions, id: \.0) { resolution in Button { vm.defaultResolution = resolution.0 } label: { HStack(spacing: 14) { VStack(alignment: .leading, spacing: 5) { Text(resolution.1) .font(optionTitleFont) .foregroundStyle(DS.Colors.textPrimary) Text(resolutionDescription(for: resolution.0)) .font(optionBodyFont) .foregroundStyle(DS.Colors.textSecondary) } Spacer() Image(systemName: viewModel.defaultResolution == resolution.0 ? "checkmark.circle.fill" : "circle") .font(.system(size: indicatorSize, weight: .bold)) .foregroundStyle(viewModel.defaultResolution == resolution.0 ? DS.Colors.interactive : DS.Colors.textQuaternary) } .padding(.horizontal, 18) .padding(.vertical, 16) .background(optionBackground(selected: viewModel.defaultResolution == resolution.0)) } .platformCardStyle() } } } settingsPanel( title: "Active Streams", subtitle: "Tile occupancy and cleanup controls for Multi-View." ) { if viewModel.activeStreams.isEmpty { Text("No active streams. Add broadcasts from the dashboard to populate the grid.") .font(optionBodyFont) .foregroundStyle(DS.Colors.textSecondary) } else { VStack(spacing: 12) { ForEach(viewModel.activeStreams) { stream in HStack(spacing: 14) { VStack(alignment: .leading, spacing: 5) { Text(stream.label) .font(optionTitleFont) .foregroundStyle(DS.Colors.textPrimary) Text(stream.game.displayTitle) .font(optionBodyFont) .foregroundStyle(DS.Colors.textSecondary) } Spacer() Button(role: .destructive) { viewModel.removeStream(id: stream.id) } label: { Label("Remove", systemImage: "trash") .font(actionFont) .foregroundStyle(.white) .padding(.horizontal, 14) .padding(.vertical, 10) .background( Capsule() .fill(DS.Colors.live.opacity(0.82)) ) } .platformCardStyle() } .padding(.horizontal, 18) .padding(.vertical, 16) .background(optionBackground(selected: false)) } } Button(role: .destructive) { viewModel.clearAllStreams() } label: { Label("Clear All Streams", systemImage: "xmark.circle.fill") .font(actionFont) .foregroundStyle(.white) .padding(.horizontal, 18) .padding(.vertical, 14) .background( Capsule() .fill(DS.Colors.live.opacity(0.82)) ) } .platformCardStyle() } } settingsPanel( title: "About", subtitle: "Build and environment context." ) { infoRow(label: "Version", value: "1.0") infoRow(label: "Player", value: "mlbserver") infoRow(label: "Active Tiles", value: "\(viewModel.activeStreams.count)/4") } } .padding(.horizontal, horizontalPadding) .padding(.vertical, verticalPadding) } } private var header: some View { VStack(alignment: .leading, spacing: 10) { Text("Settings") .font(headerTitleFont) .foregroundStyle(DS.Colors.textPrimary) Text("Playback defaults, server routing, and Multi-View controls.") .font(headerBodyFont) .foregroundStyle(DS.Colors.textSecondary) } } private func settingsPanel( title: String, subtitle: String, @ViewBuilder content: () -> Content ) -> some View { VStack(alignment: .leading, spacing: 18) { VStack(alignment: .leading, spacing: 6) { Text(title) .font(sectionTitleFont) .foregroundStyle(DS.Colors.textPrimary) Text(subtitle) .font(sectionBodyFont) .foregroundStyle(DS.Colors.textSecondary) } content() } .padding(panelPadding) .background(panelBackground) } private func infoRow(label: String, value: String) -> some View { HStack(alignment: .top, spacing: 16) { Text(label) .font(infoLabelFont) .foregroundStyle(DS.Colors.textTertiary) .frame(width: labelWidth, alignment: .leading) Text(value) .font(infoValueFont) .foregroundStyle(DS.Colors.textPrimary) Spacer(minLength: 0) } } private func optionBackground(selected: Bool) -> some View { RoundedRectangle(cornerRadius: optionRadius, style: .continuous) .fill(selected ? DS.Colors.interactive.opacity(0.14) : DS.Colors.panelFillMuted) .overlay { RoundedRectangle(cornerRadius: optionRadius, style: .continuous) .strokeBorder(selected ? DS.Colors.interactive.opacity(0.26) : DS.Colors.panelStroke, lineWidth: 1) } } private func resolutionLabel(for key: String) -> String { resolutions.first(where: { $0.0 == key })?.1 ?? key } private func resolutionDescription(for key: String) -> String { switch key { case "best": return "Uses the highest reliable stream available." case "1080p60": return "Prioritizes smooth full-HD playback when available." case "720p60": return "Balanced quality for lighter network conditions." case "540p": return "Lower bandwidth option for stability." case "adaptive": return "Lets the player adjust dynamically." default: return "Custom playback preference." } } private var panelBackground: some View { RoundedRectangle(cornerRadius: panelRadius, style: .continuous) .fill( LinearGradient( colors: [ DS.Colors.panelFill, DS.Colors.panelFillMuted, ], startPoint: .topLeading, endPoint: .bottomTrailing ) ) .overlay { RoundedRectangle(cornerRadius: panelRadius, style: .continuous) .strokeBorder(DS.Colors.panelStroke, lineWidth: 1) } .shadow(color: DS.Shadows.card, radius: DS.Shadows.cardRadius, y: DS.Shadows.cardY) } #if os(tvOS) private var horizontalPadding: CGFloat { 56 } private var verticalPadding: CGFloat { 40 } private var panelPadding: CGFloat { 24 } private var panelRadius: CGFloat { 28 } private var optionRadius: CGFloat { 22 } private var labelWidth: CGFloat { 150 } private var indicatorSize: CGFloat { 22 } private var headerTitleFont: Font { .system(size: 44, weight: .black, design: .rounded) } private var headerBodyFont: Font { .system(size: 18, weight: .medium) } private var sectionTitleFont: Font { .system(size: 28, weight: .black, design: .rounded) } private var sectionBodyFont: Font { .system(size: 16, weight: .medium) } private var infoLabelFont: Font { .system(size: 15, weight: .black, design: .rounded) } private var infoValueFont: Font { .system(size: 18, weight: .bold, design: .rounded) } private var optionTitleFont: Font { .system(size: 20, weight: .bold, design: .rounded) } private var optionBodyFont: Font { .system(size: 15, weight: .medium) } private var actionFont: Font { .system(size: 14, weight: .black, design: .rounded) } #else private var horizontalPadding: CGFloat { 20 } private var verticalPadding: CGFloat { 24 } private var panelPadding: CGFloat { 18 } private var panelRadius: CGFloat { 22 } private var optionRadius: CGFloat { 18 } private var labelWidth: CGFloat { 100 } private var indicatorSize: CGFloat { 18 } private var headerTitleFont: Font { .system(size: 28, weight: .black, design: .rounded) } private var headerBodyFont: Font { .system(size: 14, weight: .medium) } private var sectionTitleFont: Font { .system(size: 20, weight: .black, design: .rounded) } private var sectionBodyFont: Font { .system(size: 13, weight: .medium) } private var infoLabelFont: Font { .system(size: 12, weight: .black, design: .rounded) } private var infoValueFont: Font { .system(size: 14, weight: .bold, design: .rounded) } private var optionTitleFont: Font { .system(size: 16, weight: .bold, design: .rounded) } private var optionBodyFont: Font { .system(size: 12, weight: .medium) } private var actionFont: Font { .system(size: 12, weight: .black, design: .rounded) } #endif }