chore: added light and dark mode
19497d4b
3 file(s) · +161 −34
| 6 | 6 | import SwiftUI |
|
| 7 | 7 | import Combine |
|
| 8 | 8 | ||
| 9 | + | /// Appearance mode options |
|
| 10 | + | enum AppearanceMode: String, CaseIterable, Identifiable { |
|
| 11 | + | case automatic = "Automatic" |
|
| 12 | + | case light = "Light" |
|
| 13 | + | case dark = "Dark" |
|
| 14 | + | ||
| 15 | + | var id: String { rawValue } |
|
| 16 | + | ||
| 17 | + | var colorScheme: ColorScheme? { |
|
| 18 | + | switch self { |
|
| 19 | + | case .automatic: return nil |
|
| 20 | + | case .light: return .light |
|
| 21 | + | case .dark: return .dark |
|
| 22 | + | } |
|
| 23 | + | } |
|
| 24 | + | ||
| 25 | + | var icon: String { |
|
| 26 | + | switch self { |
|
| 27 | + | case .automatic: return "circle.lefthalf.filled" |
|
| 28 | + | case .light: return "sun.max.fill" |
|
| 29 | + | case .dark: return "moon.fill" |
|
| 30 | + | } |
|
| 31 | + | } |
|
| 32 | + | } |
|
| 33 | + | ||
| 9 | 34 | /// Available font design options for the browser |
|
| 10 | 35 | enum FontDesignOption: String, CaseIterable, Identifiable { |
|
| 11 | 36 | case system = "System" |
|
| 28 | 53 | /// Observable object that manages theme customization settings. |
|
| 29 | 54 | /// This provides centralized accent color management that views can subscribe to. |
|
| 30 | 55 | class ThemeSettings: ObservableObject { |
|
| 56 | + | /// The appearance mode (light, dark, or automatic) |
|
| 57 | + | @Published var appearanceMode: AppearanceMode = .automatic |
|
| 58 | + | ||
| 31 | 59 | /// The primary accent color used for interactive elements like links and buttons |
|
| 32 | 60 | @Published var accentColor: Color = .blue |
|
| 33 | 61 | ||
| 43 | 71 | /// The color used for toolbar buttons (navigation, menu, etc.) |
|
| 44 | 72 | @Published var toolbarButtonColor: Color = .blue |
|
| 45 | 73 | ||
| 46 | - | /// The background color for the main content area |
|
| 47 | - | @Published var backgroundColor: Color = Color(UIColor.systemBackground) |
|
| 74 | + | /// The background color for the main content area (light mode) |
|
| 75 | + | @Published var lightBackgroundColor: Color = .white |
|
| 76 | + | ||
| 77 | + | /// The text color for content (light mode) |
|
| 78 | + | @Published var lightTextColor: Color = .black |
|
| 79 | + | ||
| 80 | + | /// The background color for the main content area (dark mode) |
|
| 81 | + | @Published var darkBackgroundColor: Color = Color(red: 0.1, green: 0.1, blue: 0.1) |
|
| 48 | 82 | ||
| 49 | - | /// The text color for content |
|
| 50 | - | @Published var textColor: Color = Color(UIColor.label) |
|
| 83 | + | /// The text color for content (dark mode) |
|
| 84 | + | @Published var darkTextColor: Color = .white |
|
| 51 | 85 | ||
| 52 | 86 | /// The font design for content |
|
| 53 | 87 | @Published var fontDesign: FontDesignOption = .monospaced |
|
| 55 | 89 | /// The home page URL that the browser navigates to on launch and when pressing Home |
|
| 56 | 90 | @AppStorage("homePage") var homePage: String = "gemini://geminiprotocol.net/" |
|
| 57 | 91 | ||
| 58 | - | /// Key for persisting accent color hex value |
|
| 92 | + | /// Computed property for current background color based on system appearance |
|
| 93 | + | var backgroundColor: Color { |
|
| 94 | + | switch appearanceMode { |
|
| 95 | + | case .light: |
|
| 96 | + | return lightBackgroundColor |
|
| 97 | + | case .dark: |
|
| 98 | + | return darkBackgroundColor |
|
| 99 | + | case .automatic: |
|
| 100 | + | return Color(UIColor.systemBackground) |
|
| 101 | + | } |
|
| 102 | + | } |
|
| 103 | + | ||
| 104 | + | /// Computed property for current text color based on system appearance |
|
| 105 | + | var textColor: Color { |
|
| 106 | + | switch appearanceMode { |
|
| 107 | + | case .light: |
|
| 108 | + | return lightTextColor |
|
| 109 | + | case .dark: |
|
| 110 | + | return darkTextColor |
|
| 111 | + | case .automatic: |
|
| 112 | + | return Color(UIColor.label) |
|
| 113 | + | } |
|
| 114 | + | } |
|
| 115 | + | ||
| 116 | + | /// Key for persisting values |
|
| 59 | 117 | private static let accentColorKey = "accentColorHex" |
|
| 60 | - | private static let backgroundColorKey = "backgroundColorKey" |
|
| 61 | - | private static let textColorKey = "textColorHex" |
|
| 118 | + | private static let appearanceModeKey = "appearanceMode" |
|
| 119 | + | private static let lightBackgroundColorKey = "lightBackgroundColorHex" |
|
| 120 | + | private static let lightTextColorKey = "lightTextColorHex" |
|
| 121 | + | private static let darkBackgroundColorKey = "darkBackgroundColorHex" |
|
| 122 | + | private static let darkTextColorKey = "darkTextColorHex" |
|
| 62 | 123 | private static let fontDesignKey = "fontDesign" |
|
| 63 | 124 | ||
| 64 | 125 | init() { |
|
| 126 | + | if let modeRaw = UserDefaults.standard.string(forKey: Self.appearanceModeKey), |
|
| 127 | + | let mode = AppearanceMode(rawValue: modeRaw) { |
|
| 128 | + | appearanceMode = mode |
|
| 129 | + | } |
|
| 65 | 130 | if let hex = UserDefaults.standard.string(forKey: Self.accentColorKey), |
|
| 66 | 131 | let color = Color(hex: hex) { |
|
| 67 | - | setAllAccentColors(color) |
|
| 132 | + | setAllAccentColors(color, persist: false) |
|
| 133 | + | } |
|
| 134 | + | if let hex = UserDefaults.standard.string(forKey: Self.lightBackgroundColorKey), |
|
| 135 | + | let color = Color(hex: hex) { |
|
| 136 | + | lightBackgroundColor = color |
|
| 137 | + | } |
|
| 138 | + | if let hex = UserDefaults.standard.string(forKey: Self.lightTextColorKey), |
|
| 139 | + | let color = Color(hex: hex) { |
|
| 140 | + | lightTextColor = color |
|
| 68 | 141 | } |
|
| 69 | - | if let hex = UserDefaults.standard.string(forKey: Self.backgroundColorKey), |
|
| 142 | + | if let hex = UserDefaults.standard.string(forKey: Self.darkBackgroundColorKey), |
|
| 70 | 143 | let color = Color(hex: hex) { |
|
| 71 | - | backgroundColor = color |
|
| 144 | + | darkBackgroundColor = color |
|
| 72 | 145 | } |
|
| 73 | - | if let hex = UserDefaults.standard.string(forKey: Self.textColorKey), |
|
| 146 | + | if let hex = UserDefaults.standard.string(forKey: Self.darkTextColorKey), |
|
| 74 | 147 | let color = Color(hex: hex) { |
|
| 75 | - | textColor = color |
|
| 148 | + | darkTextColor = color |
|
| 76 | 149 | } |
|
| 77 | 150 | if let fontRaw = UserDefaults.standard.string(forKey: Self.fontDesignKey), |
|
| 78 | 151 | let font = FontDesignOption(rawValue: fontRaw) { |
|
| 80 | 153 | } |
|
| 81 | 154 | } |
|
| 82 | 155 | ||
| 83 | - | /// Sets all accent colors to the given color and persists the choice |
|
| 84 | - | func setAllAccentColors(_ color: Color) { |
|
| 156 | + | /// Sets all accent colors to the given color and optionally persists the choice |
|
| 157 | + | func setAllAccentColors(_ color: Color, persist: Bool = true) { |
|
| 85 | 158 | accentColor = color |
|
| 86 | 159 | progressBarColor = color |
|
| 87 | 160 | linkColor = color |
|
| 88 | 161 | mediaAccentColor = color |
|
| 89 | 162 | toolbarButtonColor = color |
|
| 90 | 163 | ||
| 91 | - | if let hex = color.toHex() { |
|
| 164 | + | if persist, let hex = color.toHex() { |
|
| 92 | 165 | UserDefaults.standard.set(hex, forKey: Self.accentColorKey) |
|
| 93 | 166 | } |
|
| 94 | 167 | } |
|
| 95 | 168 | ||
| 96 | - | /// Sets the background color and persists the choice |
|
| 97 | - | func setBackgroundColor(_ color: Color) { |
|
| 98 | - | backgroundColor = color |
|
| 169 | + | /// Sets the appearance mode and persists the choice |
|
| 170 | + | func setAppearanceMode(_ mode: AppearanceMode) { |
|
| 171 | + | appearanceMode = mode |
|
| 172 | + | UserDefaults.standard.set(mode.rawValue, forKey: Self.appearanceModeKey) |
|
| 173 | + | } |
|
| 174 | + | ||
| 175 | + | /// Sets the light mode background color and persists the choice |
|
| 176 | + | func setLightBackgroundColor(_ color: Color) { |
|
| 177 | + | lightBackgroundColor = color |
|
| 99 | 178 | if let hex = color.toHex() { |
|
| 100 | - | UserDefaults.standard.set(hex, forKey: Self.backgroundColorKey) |
|
| 179 | + | UserDefaults.standard.set(hex, forKey: Self.lightBackgroundColorKey) |
|
| 180 | + | } |
|
| 181 | + | } |
|
| 182 | + | ||
| 183 | + | /// Sets the light mode text color and persists the choice |
|
| 184 | + | func setLightTextColor(_ color: Color) { |
|
| 185 | + | lightTextColor = color |
|
| 186 | + | if let hex = color.toHex() { |
|
| 187 | + | UserDefaults.standard.set(hex, forKey: Self.lightTextColorKey) |
|
| 188 | + | } |
|
| 189 | + | } |
|
| 190 | + | ||
| 191 | + | /// Sets the dark mode background color and persists the choice |
|
| 192 | + | func setDarkBackgroundColor(_ color: Color) { |
|
| 193 | + | darkBackgroundColor = color |
|
| 194 | + | if let hex = color.toHex() { |
|
| 195 | + | UserDefaults.standard.set(hex, forKey: Self.darkBackgroundColorKey) |
|
| 101 | 196 | } |
|
| 102 | 197 | } |
|
| 103 | 198 | ||
| 104 | - | /// Sets the text color and persists the choice |
|
| 105 | - | func setTextColor(_ color: Color) { |
|
| 106 | - | textColor = color |
|
| 199 | + | /// Sets the dark mode text color and persists the choice |
|
| 200 | + | func setDarkTextColor(_ color: Color) { |
|
| 201 | + | darkTextColor = color |
|
| 107 | 202 | if let hex = color.toHex() { |
|
| 108 | - | UserDefaults.standard.set(hex, forKey: Self.textColorKey) |
|
| 203 | + | UserDefaults.standard.set(hex, forKey: Self.darkTextColorKey) |
|
| 109 | 204 | } |
|
| 110 | 205 | } |
|
| 111 | 206 | ||
| 16 | 16 | ContentView() |
|
| 17 | 17 | .environment(\.themeSettings, themeSettings) |
|
| 18 | 18 | .environmentObject(themeSettings) |
|
| 19 | + | .preferredColorScheme(themeSettings.appearanceMode.colorScheme) |
|
| 19 | 20 | } |
|
| 20 | 21 | } |
|
| 21 | 22 | } |
| 10 | 10 | @Environment(\.dismiss) private var dismiss |
|
| 11 | 11 | ||
| 12 | 12 | @State private var homePageText: String = "" |
|
| 13 | + | @State private var selectedAppearanceMode: AppearanceMode = .automatic |
|
| 13 | 14 | @State private var selectedAccentColor: Color = .blue |
|
| 14 | - | @State private var selectedBackgroundColor: Color = Color(UIColor.systemBackground) |
|
| 15 | - | @State private var selectedTextColor: Color = Color(UIColor.label) |
|
| 15 | + | @State private var selectedLightBackgroundColor: Color = .white |
|
| 16 | + | @State private var selectedLightTextColor: Color = .black |
|
| 17 | + | @State private var selectedDarkBackgroundColor: Color = .black |
|
| 18 | + | @State private var selectedDarkTextColor: Color = .white |
|
| 16 | 19 | @State private var selectedFontDesign: FontDesignOption = .monospaced |
|
| 17 | 20 | ||
| 18 | 21 | var body: some View { |
|
| 30 | 33 | } |
|
| 31 | 34 | ||
| 32 | 35 | Section { |
|
| 36 | + | Picker("Appearance", selection: $selectedAppearanceMode) { |
|
| 37 | + | ForEach(AppearanceMode.allCases) { mode in |
|
| 38 | + | Text(mode.rawValue).tag(mode) |
|
| 39 | + | } |
|
| 40 | + | } |
|
| 41 | + | } header: { |
|
| 42 | + | Text("Theme") |
|
| 43 | + | } footer: { |
|
| 44 | + | Text("Choose between light, dark, or automatic appearance.") |
|
| 45 | + | } |
|
| 46 | + | ||
| 47 | + | Section { |
|
| 33 | 48 | ColorPicker("Accent Color", selection: $selectedAccentColor, supportsOpacity: false) |
|
| 34 | - | ColorPicker("Background Color", selection: $selectedBackgroundColor, supportsOpacity: false) |
|
| 35 | - | ColorPicker("Text Color", selection: $selectedTextColor, supportsOpacity: false) |
|
| 36 | 49 | Picker("Font", selection: $selectedFontDesign) { |
|
| 37 | 50 | ForEach(FontDesignOption.allCases) { option in |
|
| 38 | 51 | Text(option.rawValue).tag(option) |
|
| 39 | 52 | } |
|
| 40 | 53 | } |
|
| 41 | 54 | } header: { |
|
| 42 | - | Text("Appearance") |
|
| 43 | - | } footer: { |
|
| 44 | - | Text("Customize the look of your browser interface.") |
|
| 55 | + | Text("General") |
|
| 56 | + | } |
|
| 57 | + | ||
| 58 | + | Section { |
|
| 59 | + | ColorPicker("Background", selection: $selectedLightBackgroundColor, supportsOpacity: false) |
|
| 60 | + | ColorPicker("Text", selection: $selectedLightTextColor, supportsOpacity: false) |
|
| 61 | + | } header: { |
|
| 62 | + | Text("Light Mode Colors") |
|
| 63 | + | } |
|
| 64 | + | ||
| 65 | + | Section { |
|
| 66 | + | ColorPicker("Background", selection: $selectedDarkBackgroundColor, supportsOpacity: false) |
|
| 67 | + | ColorPicker("Text", selection: $selectedDarkTextColor, supportsOpacity: false) |
|
| 68 | + | } header: { |
|
| 69 | + | Text("Dark Mode Colors") |
|
| 45 | 70 | } |
|
| 46 | 71 | } |
|
| 47 | 72 | .navigationTitle("Settings") |
|
| 55 | 80 | ToolbarItem(placement: .confirmationAction) { |
|
| 56 | 81 | Button("Save") { |
|
| 57 | 82 | themeSettings.homePage = homePageText |
|
| 83 | + | themeSettings.setAppearanceMode(selectedAppearanceMode) |
|
| 58 | 84 | themeSettings.setAllAccentColors(selectedAccentColor) |
|
| 59 | - | themeSettings.setBackgroundColor(selectedBackgroundColor) |
|
| 60 | - | themeSettings.setTextColor(selectedTextColor) |
|
| 85 | + | themeSettings.setLightBackgroundColor(selectedLightBackgroundColor) |
|
| 86 | + | themeSettings.setLightTextColor(selectedLightTextColor) |
|
| 87 | + | themeSettings.setDarkBackgroundColor(selectedDarkBackgroundColor) |
|
| 88 | + | themeSettings.setDarkTextColor(selectedDarkTextColor) |
|
| 61 | 89 | themeSettings.setFontDesign(selectedFontDesign) |
|
| 62 | 90 | dismiss() |
|
| 63 | 91 | } |
|
| 65 | 93 | } |
|
| 66 | 94 | .onAppear { |
|
| 67 | 95 | homePageText = themeSettings.homePage |
|
| 96 | + | selectedAppearanceMode = themeSettings.appearanceMode |
|
| 68 | 97 | selectedAccentColor = themeSettings.accentColor |
|
| 69 | - | selectedBackgroundColor = themeSettings.backgroundColor |
|
| 70 | - | selectedTextColor = themeSettings.textColor |
|
| 98 | + | selectedLightBackgroundColor = themeSettings.lightBackgroundColor |
|
| 99 | + | selectedLightTextColor = themeSettings.lightTextColor |
|
| 100 | + | selectedDarkBackgroundColor = themeSettings.darkBackgroundColor |
|
| 101 | + | selectedDarkTextColor = themeSettings.darkTextColor |
|
| 71 | 102 | selectedFontDesign = themeSettings.fontDesign |
|
| 72 | 103 | } |
|
| 73 | 104 | } |
|