Files
Sportstime/SportsTime/Features/Settings/Views/SettingsView.swift
Trey t 56869ce479 feat(ui): add 23 home screen design variants with picker
Add design style system with 23 unique home screen aesthetics:
- Classic (original SportsTime design, now default)
- 12 experimental styles (Brutalist, Luxury Editorial, etc.)
- 10 polished app-inspired styles (Flighty, SeatGeek, Apple Maps,
  Things 3, Airbnb, Spotify, Nike Run Club, Fantastical, Strava,
  Carrot Weather)

Includes settings picker to switch between styles and persists
selection via UserDefaults.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 14:44:30 -06:00

338 lines
11 KiB
Swift

//
// SettingsView.swift
// SportsTime
//
import SwiftUI
struct SettingsView: View {
@Environment(\.colorScheme) private var colorScheme
@State private var viewModel = SettingsViewModel()
@State private var showResetConfirmation = false
@State private var showPaywall = false
var body: some View {
List {
// Subscription
subscriptionSection
// Theme Selection
themeSection
// UI Design Style
designStyleSection
// Sports Preferences
sportsSection
// Travel Preferences
travelSection
// About
aboutSection
// Reset
resetSection
}
.scrollContentBackground(.hidden)
.themedBackground()
.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: - Theme Section
private var themeSection: some View {
Section {
ForEach(AppTheme.allCases) { theme in
Button {
withAnimation(.easeInOut(duration: 0.2)) {
viewModel.selectedTheme = theme
}
} label: {
HStack(spacing: 12) {
// Color preview circles
HStack(spacing: -6) {
ForEach(Array(theme.previewColors.enumerated()), id: \.offset) { _, color in
Circle()
.fill(color)
.frame(width: 24, height: 24)
.overlay(
Circle()
.stroke(Color.primary.opacity(0.1), lineWidth: 1)
)
}
}
VStack(alignment: .leading, spacing: 2) {
Text(theme.displayName)
.font(.body)
.foregroundStyle(.primary)
Text(theme.description)
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
if viewModel.selectedTheme == theme {
Image(systemName: "checkmark.circle.fill")
.foregroundStyle(Theme.warmOrange)
.font(.title3)
}
}
.contentShape(Rectangle())
}
.buttonStyle(.plain)
}
} header: {
Text("Theme")
} footer: {
Text("Choose a color scheme for the app.")
}
.listRowBackground(Theme.cardBackground(colorScheme))
}
// MARK: - Design Style Section
private var designStyleSection: some View {
Section {
ForEach(UIDesignStyle.allCases) { style in
Button {
withAnimation(.easeInOut(duration: 0.2)) {
DesignStyleManager.shared.setStyle(style)
}
} label: {
HStack(spacing: 12) {
// Icon with accent color
ZStack {
Circle()
.fill(style.accentColor.opacity(0.15))
.frame(width: 36, height: 36)
Image(systemName: style.iconName)
.font(.system(size: 16))
.foregroundStyle(style.accentColor)
}
VStack(alignment: .leading, spacing: 2) {
Text(style.rawValue)
.font(.body)
.foregroundStyle(.primary)
Text(style.description)
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(1)
}
Spacer()
if DesignStyleManager.shared.currentStyle == style {
Image(systemName: "checkmark.circle.fill")
.foregroundStyle(style.accentColor)
.font(.title3)
}
}
.contentShape(Rectangle())
}
.buttonStyle(.plain)
}
} header: {
Text("Home Screen Style")
} footer: {
Text("Choose a visual aesthetic for the home screen.")
}
.listRowBackground(Theme.cardBackground(colorScheme))
}
// 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.")
}
.listRowBackground(Theme.cardBackground(colorScheme))
}
// MARK: - Travel Section
private var travelSection: some View {
Section {
VStack(alignment: .leading, spacing: 8) {
HStack {
Text("Max Driving Per Day")
Spacer()
Text("\(viewModel.maxDrivingHoursPerDay) hours")
.foregroundStyle(.secondary)
}
Slider(
value: Binding(
get: { Double(viewModel.maxDrivingHoursPerDay) },
set: { viewModel.maxDrivingHoursPerDay = Int($0) }
),
in: 2...12,
step: 1
)
}
} header: {
Text("Travel Preferences")
} footer: {
Text("Trips will be optimized to keep daily driving within this limit.")
}
.listRowBackground(Theme.cardBackground(colorScheme))
}
// 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://88oakapps.com/privacy")!) {
Label("Privacy Policy", systemImage: "hand.raised")
}
Link(destination: URL(string: "https://88oakapps.com/terms")!) {
Label("Terms of Service", systemImage: "doc.text")
}
Link(destination: URL(string: "mailto:support@88oakapps.com")!) {
Label("Contact Support", systemImage: "envelope")
}
} header: {
Text("About")
}
.listRowBackground(Theme.cardBackground(colorScheme))
}
// MARK: - Reset Section
private var resetSection: some View {
Section {
Button(role: .destructive) {
showResetConfirmation = true
} label: {
Label("Reset to Defaults", systemImage: "arrow.counterclockwise")
}
}
.listRowBackground(Theme.cardBackground(colorScheme))
}
// MARK: - Subscription Section
private var subscriptionSection: some View {
Section {
if StoreManager.shared.isPro {
// Pro user - show manage option
HStack {
Label {
VStack(alignment: .leading, spacing: 4) {
Text("SportsTime Pro")
.foregroundStyle(Theme.textPrimary(colorScheme))
Text("Active subscription")
.font(.caption)
.foregroundStyle(.green)
}
} icon: {
Image(systemName: "star.fill")
.foregroundStyle(Theme.warmOrange)
}
Spacer()
Image(systemName: "checkmark.circle.fill")
.foregroundStyle(.green)
}
Button {
if let url = URL(string: "https://apps.apple.com/account/subscriptions") {
UIApplication.shared.open(url)
}
} label: {
Label("Manage Subscription", systemImage: "gear")
}
} else {
// Free user - show upgrade option
Button {
showPaywall = true
} label: {
HStack {
Label {
VStack(alignment: .leading, spacing: 4) {
Text("Upgrade to Pro")
.foregroundStyle(Theme.textPrimary(colorScheme))
Text("Unlimited trips, PDF export, progress tracking")
.font(.caption)
.foregroundStyle(Theme.textSecondary(colorScheme))
}
} icon: {
Image(systemName: "star.fill")
.foregroundStyle(Theme.warmOrange)
}
Spacer()
Image(systemName: "chevron.right")
.foregroundStyle(Theme.textMuted(colorScheme))
}
}
.buttonStyle(.plain)
Button {
Task {
await StoreManager.shared.restorePurchases()
}
} label: {
Label("Restore Purchases", systemImage: "arrow.clockwise")
}
}
} header: {
Text("Subscription")
}
.listRowBackground(Theme.cardBackground(colorScheme))
.sheet(isPresented: $showPaywall) {
PaywallView()
}
}
// MARK: - Helpers
private func sportColor(for sport: Sport) -> Color {
sport.themeColor
}
}
#Preview {
NavigationStack {
SettingsView()
}
}