- Three-scenario planning engine (A: date range, B: selected games, C: directional routes) - GeographicRouteExplorer with anchor game support for route exploration - Shared ItineraryBuilder for travel segment calculation - TravelEstimator for driving time/distance estimation - SwiftUI views for trip creation and detail display - CloudKit integration for schedule data - Python scraping scripts for sports schedules 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
229 lines
6.3 KiB
Swift
229 lines
6.3 KiB
Swift
//
|
|
// SettingsView.swift
|
|
// SportsTime
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
struct SettingsView: View {
|
|
@State private var viewModel = SettingsViewModel()
|
|
@State private var showResetConfirmation = false
|
|
|
|
var body: some View {
|
|
List {
|
|
// Sports Preferences
|
|
sportsSection
|
|
|
|
// Travel Preferences
|
|
travelSection
|
|
|
|
// Game Preferences
|
|
gamePreferencesSection
|
|
|
|
// Notifications
|
|
notificationsSection
|
|
|
|
// Data Sync
|
|
dataSection
|
|
|
|
// About
|
|
aboutSection
|
|
|
|
// Reset
|
|
resetSection
|
|
}
|
|
.navigationTitle("Settings")
|
|
.alert("Reset Settings", isPresented: $showResetConfirmation) {
|
|
Button("Cancel", role: .cancel) { }
|
|
Button("Reset", role: .destructive) {
|
|
viewModel.resetToDefaults()
|
|
}
|
|
} message: {
|
|
Text("This will reset all settings to their default values.")
|
|
}
|
|
}
|
|
|
|
// MARK: - Sports Section
|
|
|
|
private var sportsSection: some View {
|
|
Section {
|
|
ForEach(Sport.supported) { sport in
|
|
Toggle(isOn: Binding(
|
|
get: { viewModel.selectedSports.contains(sport) },
|
|
set: { _ in viewModel.toggleSport(sport) }
|
|
)) {
|
|
Label {
|
|
Text(sport.displayName)
|
|
} icon: {
|
|
Image(systemName: sport.iconName)
|
|
.foregroundStyle(sportColor(for: sport))
|
|
}
|
|
}
|
|
}
|
|
} header: {
|
|
Text("Favorite Sports")
|
|
} footer: {
|
|
Text("Selected sports will be shown by default in schedules and trip planning.")
|
|
}
|
|
}
|
|
|
|
// MARK: - Travel Section
|
|
|
|
private var travelSection: some View {
|
|
Section {
|
|
Stepper(value: $viewModel.maxDrivingHoursPerDay, in: 2...12) {
|
|
HStack {
|
|
Text("Max Driving Per Day")
|
|
Spacer()
|
|
Text("\(viewModel.maxDrivingHoursPerDay) hours")
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
} header: {
|
|
Text("Travel Preferences")
|
|
} footer: {
|
|
Text("Trips will be optimized to keep daily driving within this limit.")
|
|
}
|
|
}
|
|
|
|
// MARK: - Game Preferences Section
|
|
|
|
private var gamePreferencesSection: some View {
|
|
Section {
|
|
Picker("Preferred Game Time", selection: $viewModel.preferredGameTime) {
|
|
ForEach(PreferredGameTime.allCases) { time in
|
|
VStack(alignment: .leading) {
|
|
Text(time.displayName)
|
|
}
|
|
.tag(time)
|
|
}
|
|
}
|
|
|
|
Toggle("Include Playoff Games", isOn: $viewModel.includePlayoffGames)
|
|
} header: {
|
|
Text("Game Preferences")
|
|
} footer: {
|
|
Text("These preferences affect trip optimization.")
|
|
}
|
|
}
|
|
|
|
// MARK: - Notifications Section
|
|
|
|
private var notificationsSection: some View {
|
|
Section {
|
|
Toggle("Schedule Updates", isOn: $viewModel.notificationsEnabled)
|
|
} header: {
|
|
Text("Notifications")
|
|
} footer: {
|
|
Text("Get notified when games in your trips are rescheduled.")
|
|
}
|
|
}
|
|
|
|
// MARK: - Data Section
|
|
|
|
private var dataSection: some View {
|
|
Section {
|
|
Button {
|
|
Task {
|
|
await viewModel.syncSchedules()
|
|
}
|
|
} label: {
|
|
HStack {
|
|
Label("Sync Schedules", systemImage: "arrow.triangle.2.circlepath")
|
|
|
|
Spacer()
|
|
|
|
if viewModel.isSyncing {
|
|
ProgressView()
|
|
}
|
|
}
|
|
}
|
|
.disabled(viewModel.isSyncing)
|
|
|
|
if let lastSync = viewModel.lastSyncDate {
|
|
HStack {
|
|
Text("Last Sync")
|
|
Spacer()
|
|
Text(lastSync, style: .relative)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
|
|
if let error = viewModel.syncError {
|
|
HStack {
|
|
Image(systemName: "exclamationmark.triangle")
|
|
.foregroundStyle(.red)
|
|
Text(error)
|
|
.font(.caption)
|
|
.foregroundStyle(.red)
|
|
}
|
|
}
|
|
} header: {
|
|
Text("Data")
|
|
} footer: {
|
|
#if targetEnvironment(simulator)
|
|
Text("Using stub data (Simulator mode)")
|
|
#else
|
|
Text("Schedule data is synced from CloudKit.")
|
|
#endif
|
|
}
|
|
}
|
|
|
|
// MARK: - About Section
|
|
|
|
private var aboutSection: some View {
|
|
Section {
|
|
HStack {
|
|
Text("Version")
|
|
Spacer()
|
|
Text("\(viewModel.appVersion) (\(viewModel.buildNumber))")
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
|
|
Link(destination: URL(string: "https://sportstime.app/privacy")!) {
|
|
Label("Privacy Policy", systemImage: "hand.raised")
|
|
}
|
|
|
|
Link(destination: URL(string: "https://sportstime.app/terms")!) {
|
|
Label("Terms of Service", systemImage: "doc.text")
|
|
}
|
|
|
|
Link(destination: URL(string: "mailto:support@sportstime.app")!) {
|
|
Label("Contact Support", systemImage: "envelope")
|
|
}
|
|
} header: {
|
|
Text("About")
|
|
}
|
|
}
|
|
|
|
// MARK: - Reset Section
|
|
|
|
private var resetSection: some View {
|
|
Section {
|
|
Button(role: .destructive) {
|
|
showResetConfirmation = true
|
|
} label: {
|
|
Label("Reset to Defaults", systemImage: "arrow.counterclockwise")
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Helpers
|
|
|
|
private func sportColor(for sport: Sport) -> Color {
|
|
switch sport {
|
|
case .mlb: return .red
|
|
case .nba: return .orange
|
|
case .nhl: return .blue
|
|
case .nfl: return .green
|
|
case .mls: return .purple
|
|
}
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
NavigationStack {
|
|
SettingsView()
|
|
}
|
|
}
|