diff --git a/SportsTime/Core/Theme/Theme.swift b/SportsTime/Core/Theme/Theme.swift index fdb4103..2100430 100644 --- a/SportsTime/Core/Theme/Theme.swift +++ b/SportsTime/Core/Theme/Theme.swift @@ -7,26 +7,119 @@ import SwiftUI +// MARK: - App Theme Selection + +enum AppTheme: String, CaseIterable, Identifiable { + case teal = "Teal" + case orbit = "Orbit" + case retro = "Retro" + case clutch = "Clutch" + case monochrome = "Monochrome" + case sunset = "Sunset" + case midnight = "Midnight" + + var id: String { rawValue } + + var displayName: String { rawValue } + + var description: String { + switch self { + case .teal: return "Cool cyan and teal tones" + case .orbit: return "Bold navy and orange" + case .retro: return "Classic columbia blue" + case .clutch: return "Championship red and gold" + case .monochrome: return "Clean grayscale aesthetic" + case .sunset: return "Warm oranges and purples" + case .midnight: return "Deep blues and gold" + } + } + + var previewColors: [Color] { + switch self { + case .teal: return [Color(hex: "4ECDC4"), Color(hex: "1A535C"), Color(hex: "FFE66D")] + case .orbit: return [Color(hex: "EB6E1F"), Color(hex: "002D62"), Color(hex: "FFFFFF")] + case .retro: return [Color(hex: "418FDE"), Color(hex: "C41E3A"), Color(hex: "FFFFFF")] + case .clutch: return [Color(hex: "CE1141"), Color(hex: "FDB927"), Color(hex: "041E42")] + case .monochrome: return [Color(hex: "6B7280"), Color(hex: "1F2937"), Color(hex: "F9FAFB")] + case .sunset: return [Color(hex: "F97316"), Color(hex: "7C3AED"), Color(hex: "EC4899")] + case .midnight: return [Color(hex: "3B82F6"), Color(hex: "1E3A5F"), Color(hex: "F59E0B")] + } + } +} + +// MARK: - Theme Manager + +@Observable +final class ThemeManager { + static let shared = ThemeManager() + + var currentTheme: AppTheme { + didSet { + UserDefaults.standard.set(currentTheme.rawValue, forKey: "selectedTheme") + } + } + + private init() { + if let saved = UserDefaults.standard.string(forKey: "selectedTheme"), + let theme = AppTheme(rawValue: saved) { + self.currentTheme = theme + } else { + self.currentTheme = .teal + } + } +} + // MARK: - Theme enum Theme { - // MARK: - Accent Colors (Same in Light/Dark) + private static var current: AppTheme { ThemeManager.shared.currentTheme } - static let warmOrange = Color(hex: "4ECDC4") // Strong Cyan (primary accent) - static let warmOrangeGlow = Color(hex: "6ED9D1") // Lighter cyan glow + // MARK: - Primary Accent Color - static let routeGold = Color(hex: "FFE66D") // Royal Gold - static let routeAmber = Color(hex: "FF6B6B") // Grapefruit Pink + static var warmOrange: Color { + switch current { + case .teal: return Color(hex: "4ECDC4") + case .orbit: return Color(hex: "EB6E1F") + case .retro: return Color(hex: "418FDE") + case .clutch: return Color(hex: "CE1141") + case .monochrome: return Color(hex: "6B7280") + case .sunset: return Color(hex: "F97316") + case .midnight: return Color(hex: "3B82F6") + } + } - // New palette colors - static let primaryCyan = Color(hex: "4ECDC4") // Strong Cyan - static let darkTeal = Color(hex: "1A535C") // Dark Teal - static let mintCream = Color(hex: "F7FFF7") // Mint Cream - static let grapefruit = Color(hex: "FF6B6B") // Grapefruit Pink - static let royalGold = Color(hex: "FFE66D") // Royal Gold + static var warmOrangeGlow: Color { + warmOrange.opacity(0.7) + } - // MARK: - Sport Colors + // MARK: - Secondary Accent Colors + + static var routeGold: Color { + switch current { + case .teal: return Color(hex: "FFE66D") + case .orbit: return Color(hex: "002D62") + case .retro: return Color(hex: "C41E3A") + case .clutch: return Color(hex: "FDB927") + case .monochrome: return Color(hex: "9CA3AF") + case .sunset: return Color(hex: "7C3AED") + case .midnight: return Color(hex: "F59E0B") + } + } + + static var routeAmber: Color { + switch current { + case .teal: return Color(hex: "FF6B6B") + case .orbit: return Color(hex: "EB6E1F") + case .retro: return Color(hex: "418FDE") + case .clutch: return Color(hex: "CE1141") + case .monochrome: return Color(hex: "4B5563") + case .sunset: return Color(hex: "EC4899") + case .midnight: return Color(hex: "60A5FA") + } + } + + // MARK: - Sport Colors (constant across themes) static let mlbRed = Color(hex: "E31937") static let nbaOrange = Color(hex: "F58426") @@ -36,25 +129,163 @@ enum Theme { // MARK: - Dark Mode Colors - static let darkBackground1 = Color(hex: "1A535C") // Dark Teal - static let darkBackground2 = Color(hex: "143F46") // Darker teal - static let darkCardBackground = Color(hex: "1E5A64") // Slightly lighter teal - static let darkCardBackgroundLight = Color(hex: "2A6B75") // Card elevated - static let darkSurfaceGlow = Color(hex: "4ECDC4").opacity(0.15) // Cyan glow - static let darkTextPrimary = Color(hex: "F7FFF7") // Mint Cream - static let darkTextSecondary = Color(hex: "B8E8E4") // Light cyan-tinted - static let darkTextMuted = Color(hex: "7FADA8") // Muted teal + static var darkBackground1: Color { + switch current { + case .teal: return Color(hex: "1A535C") + case .orbit: return Color(hex: "002D62") + case .retro: return Color(hex: "1A3A5C") + case .clutch: return Color(hex: "041E42") + case .monochrome: return Color(hex: "111827") + case .sunset: return Color(hex: "1F1033") + case .midnight: return Color(hex: "0F172A") + } + } + + static var darkBackground2: Color { + switch current { + case .teal: return Color(hex: "143F46") + case .orbit: return Color(hex: "001A3A") + case .retro: return Color(hex: "0F2840") + case .clutch: return Color(hex: "020E1F") + case .monochrome: return Color(hex: "0D1117") + case .sunset: return Color(hex: "150A24") + case .midnight: return Color(hex: "020617") + } + } + + static var darkCardBackground: Color { + switch current { + case .teal: return Color(hex: "1E5A64") + case .orbit: return Color(hex: "0A3A6E") + case .retro: return Color(hex: "234B6E") + case .clutch: return Color(hex: "0A2847") + case .monochrome: return Color(hex: "1F2937") + case .sunset: return Color(hex: "2D1B4E") + case .midnight: return Color(hex: "1E3A5F") + } + } + + static var darkCardBackgroundLight: Color { + switch current { + case .teal: return Color(hex: "2A6B75") + case .orbit: return Color(hex: "154A7E") + case .retro: return Color(hex: "2E5A7E") + case .clutch: return Color(hex: "153557") + case .monochrome: return Color(hex: "374151") + case .sunset: return Color(hex: "3D2B5E") + case .midnight: return Color(hex: "2A4A6F") + } + } + + static var darkSurfaceGlow: Color { + warmOrange.opacity(0.15) + } + + static var darkTextPrimary: Color { + switch current { + case .teal: return Color(hex: "F7FFF7") + case .orbit: return Color(hex: "FFFFFF") + case .retro: return Color(hex: "FFFFFF") + case .clutch: return Color(hex: "FFFFFF") + case .monochrome: return Color(hex: "F9FAFB") + case .sunset: return Color(hex: "FFF7ED") + case .midnight: return Color(hex: "F8FAFC") + } + } + + static var darkTextSecondary: Color { + switch current { + case .teal: return Color(hex: "B8E8E4") + case .orbit: return Color(hex: "FFB380") + case .retro: return Color(hex: "A8C8E8") + case .clutch: return Color(hex: "FFD080") + case .monochrome: return Color(hex: "D1D5DB") + case .sunset: return Color(hex: "FED7AA") + case .midnight: return Color(hex: "93C5FD") + } + } + + static var darkTextMuted: Color { + switch current { + case .teal: return Color(hex: "7FADA8") + case .orbit: return Color(hex: "8090A0") + case .retro: return Color(hex: "7898B8") + case .clutch: return Color(hex: "8898A8") + case .monochrome: return Color(hex: "6B7280") + case .sunset: return Color(hex: "9D8AA8") + case .midnight: return Color(hex: "64748B") + } + } // MARK: - Light Mode Colors - static let lightBackground1 = Color(hex: "F7FFF7") // Mint Cream - static let lightBackground2 = Color(hex: "E8F8F5") // Slightly darker mint - static let lightCardBackground = Color.white - static let lightCardBackgroundElevated = Color(hex: "F7FFF7") // Mint cream - static let lightSurfaceBorder = Color(hex: "4ECDC4").opacity(0.3) // Cyan border - static let lightTextPrimary = Color(hex: "1A535C") // Dark Teal - static let lightTextSecondary = Color(hex: "2A6B75") // Medium teal - static let lightTextMuted = Color(hex: "5A9A94") // Light teal + static var lightBackground1: Color { + switch current { + case .teal: return Color(hex: "F7FFF7") + case .orbit: return Color(hex: "FFF8F5") + case .retro: return Color(hex: "F5F9FF") + case .clutch: return Color(hex: "FFFAF5") + case .monochrome: return Color(hex: "F9FAFB") + case .sunset: return Color(hex: "FFFBF5") + case .midnight: return Color(hex: "F8FAFC") + } + } + + static var lightBackground2: Color { + switch current { + case .teal: return Color(hex: "E8F8F5") + case .orbit: return Color(hex: "FFF0E8") + case .retro: return Color(hex: "E8F0FF") + case .clutch: return Color(hex: "FFF0E8") + case .monochrome: return Color(hex: "F3F4F6") + case .sunset: return Color(hex: "FFF3E8") + case .midnight: return Color(hex: "EFF6FF") + } + } + + static var lightCardBackground: Color { .white } + + static var lightCardBackgroundElevated: Color { lightBackground1 } + + static var lightSurfaceBorder: Color { + warmOrange.opacity(0.3) + } + + static var lightTextPrimary: Color { + switch current { + case .teal: return Color(hex: "1A535C") + case .orbit: return Color(hex: "002D62") + case .retro: return Color(hex: "1A3A5C") + case .clutch: return Color(hex: "041E42") + case .monochrome: return Color(hex: "111827") + case .sunset: return Color(hex: "431407") + case .midnight: return Color(hex: "1E3A5F") + } + } + + static var lightTextSecondary: Color { + switch current { + case .teal: return Color(hex: "2A6B75") + case .orbit: return Color(hex: "1A4A7A") + case .retro: return Color(hex: "2A5A7C") + case .clutch: return Color(hex: "1A3A5A") + case .monochrome: return Color(hex: "374151") + case .sunset: return Color(hex: "7C2D12") + case .midnight: return Color(hex: "2A4A6F") + } + } + + static var lightTextMuted: Color { + switch current { + case .teal: return Color(hex: "5A9A94") + case .orbit: return Color(hex: "5A7A9A") + case .retro: return Color(hex: "5A8AAA") + case .clutch: return Color(hex: "6A7A8A") + case .monochrome: return Color(hex: "6B7280") + case .sunset: return Color(hex: "9A6A5A") + case .midnight: return Color(hex: "64748B") + } + } // MARK: - Adaptive Gradients diff --git a/SportsTime/Features/Settings/ViewModels/SettingsViewModel.swift b/SportsTime/Features/Settings/ViewModels/SettingsViewModel.swift index 1ebab8d..f5f5eeb 100644 --- a/SportsTime/Features/Settings/ViewModels/SettingsViewModel.swift +++ b/SportsTime/Features/Settings/ViewModels/SettingsViewModel.swift @@ -12,6 +12,12 @@ final class SettingsViewModel { // MARK: - User Preferences (persisted via UserDefaults) + var selectedTheme: AppTheme { + didSet { + ThemeManager.shared.currentTheme = selectedTheme + } + } + var selectedSports: Set { didSet { savePreferences() } } @@ -41,6 +47,9 @@ final class SettingsViewModel { // Load from UserDefaults using local variables first let defaults = UserDefaults.standard + // Theme + self.selectedTheme = ThemeManager.shared.currentTheme + // Selected sports if let sportStrings = defaults.stringArray(forKey: "selectedSports") { self.selectedSports = Set(sportStrings.compactMap { Sport(rawValue: $0) }) @@ -93,6 +102,7 @@ final class SettingsViewModel { } func resetToDefaults() { + selectedTheme = .teal selectedSports = Set(Sport.supported) maxDrivingHoursPerDay = 8 maxTripOptions = 10 diff --git a/SportsTime/Features/Settings/Views/SettingsView.swift b/SportsTime/Features/Settings/Views/SettingsView.swift index 4528c6a..e7f0770 100644 --- a/SportsTime/Features/Settings/Views/SettingsView.swift +++ b/SportsTime/Features/Settings/Views/SettingsView.swift @@ -11,6 +11,9 @@ struct SettingsView: View { var body: some View { List { + // Theme Selection + themeSection + // Sports Preferences sportsSection @@ -37,6 +40,58 @@ struct SettingsView: View { } } + // 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.") + } + } + // MARK: - Sports Section private var sportsSection: some View {