diff --git a/SportsTime/Core/Theme/Theme.swift b/SportsTime/Core/Theme/Theme.swift index 6f415a4..47653ff 100644 --- a/SportsTime/Core/Theme/Theme.swift +++ b/SportsTime/Core/Theme/Theme.swift @@ -69,6 +69,65 @@ final class ThemeManager { } } +// MARK: - Appearance Mode + +enum AppearanceMode: String, CaseIterable, Identifiable { + case system = "System" + case light = "Light" + case dark = "Dark" + + var id: String { rawValue } + + var displayName: String { rawValue } + + var iconName: String { + switch self { + case .system: return "circle.lefthalf.filled" + case .light: return "sun.max.fill" + case .dark: return "moon.fill" + } + } + + var description: String { + switch self { + case .system: return "Match device settings" + case .light: return "Always use light mode" + case .dark: return "Always use dark mode" + } + } + + /// Returns the ColorScheme to apply, or nil for system default + var colorScheme: ColorScheme? { + switch self { + case .system: return nil + case .light: return .light + case .dark: return .dark + } + } +} + +// MARK: - Appearance Manager + +@Observable +final class AppearanceManager { + static let shared = AppearanceManager() + + var currentMode: AppearanceMode { + didSet { + UserDefaults.standard.set(currentMode.rawValue, forKey: "appearanceMode") + } + } + + private init() { + if let saved = UserDefaults.standard.string(forKey: "appearanceMode"), + let mode = AppearanceMode(rawValue: saved) { + self.currentMode = mode + } else { + self.currentMode = .system + } + } +} + // MARK: - Theme enum Theme { diff --git a/SportsTime/Features/Settings/ViewModels/SettingsViewModel.swift b/SportsTime/Features/Settings/ViewModels/SettingsViewModel.swift index c34cd5e..8e2b497 100644 --- a/SportsTime/Features/Settings/ViewModels/SettingsViewModel.swift +++ b/SportsTime/Features/Settings/ViewModels/SettingsViewModel.swift @@ -72,6 +72,7 @@ final class SettingsViewModel { selectedTheme = .teal selectedSports = Set(Sport.supported) maxDrivingHoursPerDay = 8 + AppearanceManager.shared.currentMode = .system } // MARK: - Persistence diff --git a/SportsTime/Features/Settings/Views/SettingsView.swift b/SportsTime/Features/Settings/Views/SettingsView.swift index eb64b7f..b5c4d6e 100644 --- a/SportsTime/Features/Settings/Views/SettingsView.swift +++ b/SportsTime/Features/Settings/Views/SettingsView.swift @@ -17,6 +17,9 @@ struct SettingsView: View { // Subscription subscriptionSection + // Appearance Mode (Light/Dark/System) + appearanceSection + // Theme Selection themeSection @@ -55,6 +58,57 @@ struct SettingsView: View { } } + // MARK: - Appearance Section + + private var appearanceSection: some View { + Section { + ForEach(AppearanceMode.allCases) { mode in + Button { + withAnimation(.easeInOut(duration: 0.2)) { + AppearanceManager.shared.currentMode = mode + } + } label: { + HStack(spacing: 12) { + // Icon + ZStack { + Circle() + .fill(Theme.warmOrange.opacity(0.15)) + .frame(width: 32, height: 32) + + Image(systemName: mode.iconName) + .font(.system(size: 16)) + .foregroundStyle(Theme.warmOrange) + } + + VStack(alignment: .leading, spacing: 2) { + Text(mode.displayName) + .font(.body) + .foregroundStyle(.primary) + Text(mode.description) + .font(.caption) + .foregroundStyle(.secondary) + } + + Spacer() + + if AppearanceManager.shared.currentMode == mode { + Image(systemName: "checkmark.circle.fill") + .foregroundStyle(Theme.warmOrange) + .font(.title3) + } + } + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + } + } header: { + Text("Appearance") + } footer: { + Text("Choose light, dark, or follow your device settings.") + } + .listRowBackground(Theme.cardBackground(colorScheme)) + } + // MARK: - Theme Section private var themeSection: some View { diff --git a/SportsTime/SportsTimeApp.swift b/SportsTime/SportsTimeApp.swift index 91a3fd8..7cb1a69 100644 --- a/SportsTime/SportsTimeApp.swift +++ b/SportsTime/SportsTimeApp.swift @@ -82,6 +82,7 @@ struct BootstrappedContentView: View { @State private var hasCompletedInitialSync = false @State private var showOnboardingPaywall = false @State private var deepLinkHandler = DeepLinkHandler.shared + @State private var appearanceManager = AppearanceManager.shared private var shouldShowOnboardingPaywall: Bool { !UserDefaults.standard.bool(forKey: "hasSeenOnboardingPaywall") && !StoreManager.shared.isPro @@ -142,6 +143,7 @@ struct BootstrappedContentView: View { break } } + .preferredColorScheme(appearanceManager.currentMode.colorScheme) } @MainActor